Initial commit
This commit is contained in:
commit
9d9d8ce7d5
19 changed files with 399 additions and 0 deletions
35
internal/builtins/cd/cd.go
Normal file
35
internal/builtins/cd/cd.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
package cd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"gosh/internal/paths"
|
||||
)
|
||||
|
||||
func fmtError(dir string, err error) string {
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return fmt.Sprintf("directory \"%s\" does not exist", dir)
|
||||
case errors.Is(err, os.ErrPermission):
|
||||
return fmt.Sprintf("Permission denied: \"%s\"", dir)
|
||||
case errors.Is(err, syscall.ENOTDIR):
|
||||
return fmt.Sprintf("\"%s\" is not a directory", dir)
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
func Exec(args []string) error {
|
||||
dir := ""
|
||||
if len(args) > 0 {
|
||||
dir = args[0]
|
||||
}
|
||||
|
||||
dir = paths.Expand(dir)
|
||||
if err := os.Chdir(dir); err != nil {
|
||||
return errors.New("cd: " + fmtError(dir, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
3
internal/builtins/doc.go
Normal file
3
internal/builtins/doc.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// Package builtins registers and implements shell builtin commands such as
|
||||
// cd and exit that run within the current process.
|
||||
package builtins
|
||||
26
internal/builtins/exit/exit.go
Normal file
26
internal/builtins/exit/exit.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package exit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func Exec(args []string) error {
|
||||
if len(args) > 1 {
|
||||
return errors.New("exit: ")
|
||||
}
|
||||
|
||||
code := 0
|
||||
if len(args) > 0 {
|
||||
number, err := strconv.ParseUint(args[0], 10, 7)
|
||||
if err != nil || number > 125 {
|
||||
return fmt.Errorf("exit: %s must be an integer between 0 and 125", args[0])
|
||||
}
|
||||
code = int(number)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
return nil
|
||||
}
|
||||
18
internal/builtins/registry.go
Normal file
18
internal/builtins/registry.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package builtins
|
||||
|
||||
import (
|
||||
"gosh/internal/builtins/cd"
|
||||
"gosh/internal/builtins/exit"
|
||||
)
|
||||
|
||||
type builtinFn func([]string) error
|
||||
|
||||
var registry = map[string]builtinFn{
|
||||
"cd": cd.Exec,
|
||||
"exit": exit.Exec,
|
||||
}
|
||||
|
||||
func Lookup(program string) (builtinFn, bool) {
|
||||
fn, ok := registry[program]
|
||||
return fn, ok
|
||||
}
|
||||
22
internal/command/def.go
Normal file
22
internal/command/def.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package command
|
||||
|
||||
type Definition []string
|
||||
|
||||
func (cmd Definition) Valid() bool {
|
||||
return len(cmd) > 0
|
||||
}
|
||||
|
||||
func (cmd Definition) Name() string {
|
||||
return cmd[0]
|
||||
}
|
||||
|
||||
func (cmd Definition) Arguments() []string {
|
||||
if len(cmd) > 1 {
|
||||
return cmd[1:]
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (cmd Definition) Argument(i int) string {
|
||||
return cmd[i+1]
|
||||
}
|
||||
3
internal/command/doc.go
Normal file
3
internal/command/doc.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// Package command defines the command.Definition type and helpers for
|
||||
// accessing command names and arguments.
|
||||
package command
|
||||
3
internal/parser/doc.go
Normal file
3
internal/parser/doc.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// Package parser tokenizes raw user input and converts it into a
|
||||
// command.Definition for subsequent execution.
|
||||
package parser
|
||||
18
internal/parser/parse.go
Normal file
18
internal/parser/parse.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
|
||||
"gosh/internal/command"
|
||||
)
|
||||
|
||||
func Parse(input string) command.Definition {
|
||||
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||
scanner.Split(bufio.ScanWords)
|
||||
args := []string{}
|
||||
for scanner.Scan() {
|
||||
args = append(args, scanner.Text())
|
||||
}
|
||||
return args
|
||||
}
|
||||
20
internal/paths/expand.go
Normal file
20
internal/paths/expand.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Expand resolves shell-like shortcuts in a path string:
|
||||
// - empty string resolves to the user's home directory
|
||||
// - leading '~' expands to the user's home directory
|
||||
func Expand(dir string) string {
|
||||
// Empty string resolves to the users home dir.
|
||||
if len(dir) < 1 {
|
||||
return HomeDir()
|
||||
}
|
||||
// Expand ~ to users home dir.
|
||||
if dir[0] == '~' {
|
||||
return filepath.Join(HomeDir(), dir[1:])
|
||||
}
|
||||
return dir
|
||||
}
|
||||
26
internal/paths/home.go
Normal file
26
internal/paths/home.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// HomeDir returns the current user's home directory.
|
||||
// If it cannot be determined, returns an empty string.
|
||||
func HomeDir() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return home
|
||||
}
|
||||
|
||||
// AbbreviateHome replaces the user's home directory prefix with '~'
|
||||
// to produce a shorter, more readable path for display.
|
||||
func AbbreviateHome(p string) string {
|
||||
home := HomeDir()
|
||||
if home == "" {
|
||||
return p
|
||||
}
|
||||
if after, found := strings.CutPrefix(p, home); found {
|
||||
return "~" + after
|
||||
}
|
||||
return p
|
||||
}
|
||||
3
internal/prompt/doc.go
Normal file
3
internal/prompt/doc.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// Package prompt expands shell prompt templates by resolving variables like
|
||||
// %u (user), %h (host), and %w (working directory).
|
||||
package prompt
|
||||
40
internal/prompt/parse.go
Normal file
40
internal/prompt/parse.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package prompt
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gosh/internal/paths"
|
||||
)
|
||||
|
||||
func cwd(abbr bool) string {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "?"
|
||||
}
|
||||
|
||||
if abbr {
|
||||
cwd = paths.AbbreviateHome(cwd)
|
||||
}
|
||||
return cwd
|
||||
}
|
||||
|
||||
func hostname() string {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return "?"
|
||||
}
|
||||
return hostname
|
||||
}
|
||||
|
||||
func resolve(input, variable, value string) string {
|
||||
return strings.ReplaceAll(input, variable, value)
|
||||
}
|
||||
|
||||
func Parse(prompt string) string {
|
||||
prompt = resolve(prompt, "%w", cwd(true))
|
||||
prompt = resolve(prompt, "%W", cwd(false))
|
||||
prompt = resolve(prompt, "%h", hostname())
|
||||
prompt = resolve(prompt, "%u", os.ExpandEnv("$USER"))
|
||||
return prompt
|
||||
}
|
||||
3
internal/runner/doc.go
Normal file
3
internal/runner/doc.go
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// Package runner executes command definitions by dispatching builtins
|
||||
// or starting external programs in new processes.
|
||||
package runner
|
||||
26
internal/runner/runner.go
Normal file
26
internal/runner/runner.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package runner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"gosh/internal/builtins"
|
||||
"gosh/internal/command"
|
||||
)
|
||||
|
||||
func cmd(def command.Definition) *exec.Cmd {
|
||||
cmd := exec.Command(def.Name(), def.Arguments()...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Exec runs the provided command definition immediately. It is a thin wrapper
|
||||
// around Resolve for convenience and backward compatibility.
|
||||
func Exec(def command.Definition) error {
|
||||
if builtin, found := builtins.Lookup(def.Name()); found {
|
||||
return builtin(def.Arguments())
|
||||
}
|
||||
return cmd(def).Run()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue