Compare commits

...

33 Commits
v1.0.0 ... 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
Alexandre1a
47761fae5d
Update go.yml
fixed the changelog not being displayed
2025-03-19 15:28:00 +01:00
Alexandre1a
1f19d9b602
Update go.yml
Deleted build.
Re added AMD64 into muliarch
2025-03-19 15:25:11 +01:00
Alexandre1a
9a9d0979de
Update go.yml
Removed the Linux64 build from multiarch as it's already built in the previous step
2025-03-19 15:20:39 +01:00
Alexandre1a
8be9ac9e82
Update go.yml
Fixed the indentation, and added a release overview
2025-03-19 15:18:54 +01:00
Alexandre
c882c891d4 Removed the Windows Build because tests proved that Windows environement is really different 2025-02-03 14:58:34 +01:00
Alexandre
5c2adeb8ec Fixed a problem with 'ghosting' in the shell 2025-02-03 14:50:48 +01:00
Alexandre
b59fb4bf51 Changed the README to tell all avaiable colors 2025-02-03 14:45:13 +01:00
Alexandre
84b59ca9be Added prompt colors 2025-02-03 14:43:28 +01:00
Alexandre
73b0429195 Merge branch 'main' of github.com:Alexandre1a/GoSH
Again, I forgot
2025-02-03 14:03:34 +01:00
Alexandre
2b76386641 Added the Windows version for testing 2025-02-03 14:03:11 +01:00
Alexandre1a
50d280ec5f
Update README.md 2025-02-03 09:57:55 +01:00
Alexandre
9d406f8dfc Finally fixed the bug where the config won't be created 2025-02-03 09:54:58 +01:00
Alexandre
a03c941412 Changed the behaviour of argument handling 2025-02-03 09:16:35 +01:00
Alexandre
d3da40abd3 Some Changes 2025-02-03 09:14:36 +01:00
Alexandre
049e3218f3 Changed the README 2025-02-03 09:08:07 +01:00
Alexandre
b3c5d75f9a Added the pseudo TTY requirement for ssh, nano, vim but signals are not supported yet 2025-02-03 08:42:50 +01:00
Alexandre
e64ed1373c Merge branch 'main' of github.com:Alexandre1a/GoSH
I forgot to pull before editing the README
2025-02-02 22:41:30 +01:00
Alexandre
8f91a7a7c4 Added a new tab in the readme 2025-02-02 22:40:34 +01:00
Alexandre1a
7dcafec92c
Update README.md 2025-01-31 18:52:51 +01:00
Alexandre
f43bcb2954 Added defaults.toml for reference 2025-01-31 18:51:08 +01:00
Alexandre
0cb4079560 Changed the README to update it to the current state of the project 2025-01-31 17:04:55 +01:00
Alexandre
f23e5c3cab "aaa" 2025-01-31 14:01:40 +01:00
Alexandre
ca29c1c439 Added path inside the prompt 2025-01-31 08:47:23 +01:00
Alexandre
19c1d61a45 Tried to modify the token 2025-01-30 21:10:58 +01:00
8 changed files with 727 additions and 56 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -8,22 +8,6 @@ on:
branches: ["main"]
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:
runs-on: ubuntu-latest
steps:
@ -32,22 +16,25 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.23.5"
go-version: "1.24.1"
- name: Build binaries for multiple architectures
run: |
mkdir -p dist
# Linux
GOOS=linux GOARCH=amd64 go build -o dist/gosh-linux-amd64
GOOS=linux GOARCH=arm64 go build -o dist/gosh-linux-arm64
# macOS
GOOS=darwin GOARCH=amd64 go build -o dist/gosh-mac-amd64
GOOS=darwin GOARCH=arm64 go build -o dist/gosh-mac-arm64
GOOS=darwin GOARCH=amd64 go build -o dist/gosh-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -o dist/gosh-darwin-arm64
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
uses: actions/upload-artifact@v4
with:
@ -67,9 +54,55 @@ jobs:
name: gosh-binaries
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
uses: softprops/action-gh-release@v1
with:
files: dist/*
body: |
**Changelog**
${{ steps.changelog.outputs.CHANGELOG }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write # Permet de manipuler le contenu du repo
issues: write # Permet de créer des issues, si nécessaire pour le release
pull-requests: write # Autorise la gestion des PRs
actions: write # Permet de gérer les workflows
pages: write # Si tu utilises GitHub Pages
discussions: write # Si tu utilises Discussions

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

@ -1,8 +1,58 @@
# GoSh!
A Shell made in Go, for fun
## Features
At the moment, there isn't much features.
I'm planning to add more features later (like syntax hilighting, a history file and a RC file, etc...).
- Display Working Directory
- History file wich you can browse for past commands
- Some colors
- Config file
- PTY support for interactive commands
I'm planning to add more features later (like syntax hilighting, etc...).
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.
If not, the issue will be fixed soon (in case of a typo for example)
If not, the issue will be fixed soon (in case of a typo for example)
## Installation
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
`go build -o bin/GoSH`
To test the shell and see if it suits you.
If everything works, then move the binary to a place inside your path.
To directly install it with the Go toolchain, just use
`go install`
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 !
## Usage
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).~~
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.
To change the color of the prompt use `set color <color>`
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 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)

3
defaults.toml Normal file
View File

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

23
go.mod
View File

@ -4,5 +4,26 @@ go 1.23.5
require (
github.com/chzyer/readline v1.5.1 // indirect
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
github.com/creack/pty v1.1.24 // 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/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.19.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

58
go.sum
View File

@ -2,5 +2,63 @@ 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/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
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.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

539
main.go
View File

@ -2,23 +2,76 @@ package main
import (
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/chzyer/readline"
"github.com/creack/pty"
"github.com/google/shlex"
"github.com/spf13/viper"
)
// Structure pour stocker la configuration
type Config struct {
Prompt string `mapstructure:"prompt"`
Color string `mapstructure:"color"`
HistorySize int `mapstructure:"history_size"`
Aliases map[string]string `mapstructure:"aliases"`
}
// Constantes pour la version et le nom du shell
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() {
// 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: "> ",
HistoryFile: historyFile, // Permet de sauvegarder et charger l'historique
AutoComplete: nil, // Peut être amélioré avec l'autocomplétion
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)
@ -26,51 +79,477 @@ func main() {
}
defer rl.Close()
fmt.Printf("Bienvenue dans %s version %s\nTapez 'help' pour afficher l'aide.\n\n", SHELL_NAME, VERSION)
for {
// Lecture de l'entrée utilisateur avec édition et historique
rl.SetPrompt(getPrompt())
input, err := rl.Readline()
if err != nil { // EOF ou Ctrl+D
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
}
// Suppression des espaces inutiles
input = strings.TrimSpace(input)
if input == "" {
continue
}
// Exécute la commande
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 := 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)
}
viper.AddConfigPath(configPath)
viper.SetConfigName("gosh_config")
viper.SetConfigType("toml")
// Valeurs par défaut
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...")
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:", configFilePath)
}
} else {
fmt.Fprintln(os.Stderr, "Erreur de configuration:", err)
fmt.Println("Utilisation des valeurs par défaut.")
}
}
if err := viper.Unmarshal(&config); err != nil {
fmt.Fprintln(os.Stderr, "Erreur de chargement de la configuration:", err)
fmt.Println("Utilisation des valeurs par défaut.")
}
if config.HistorySize <= 0 {
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
func getPrompt() string {
wd, err := os.Getwd()
if err != nil {
wd = "?"
}
homeDir, _ := os.UserHomeDir()
if homeDir != "" && strings.HasPrefix(wd, homeDir) {
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 {
prompt = colorCode + prompt + colors["reset"]
}
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 {
input = strings.TrimSuffix(input, "\n")
args := strings.Split(input, " ")
// Remplacer les alias
expandedInput := replaceAliases(input)
if expandedInput != input {
fmt.Printf("Alias expanded: %s\n", expandedInput)
input = expandedInput
}
// Gérer les commandes intégrées
switch args[0] {
case "cd":
if len(args) < 2 || args[1] == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
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 1.0.0")
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
}
// Exécuter la commande système
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 os.Chdir(homeDir)
}
// 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 <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.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return cmd.Run()
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.Stdin = os.Stdin
// 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
func setConfig(key, value string) error {
switch key {
case "prompt":
viper.Set("prompt", value)
case "color":
if _, exists := colors[value]; !exists {
return fmt.Errorf("couleur inconnue: %s. Couleurs disponibles: %v", value, getAvailableColors())
}
viper.Set("color", value)
case "history_size":
intValue, err := strconv.Atoi(value)
if err != nil || intValue <= 0 {
return fmt.Errorf("history_size doit être un entier positif")
}
viper.Set("history_size", intValue)
default:
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)
}
if err := viper.Unmarshal(&config); err != nil {
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 {
if k != "reset" {
keys = append(keys, k)
}
}
return keys
}