This repository has been archived on 2026-03-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
oubliette/internal/shell/bash/commands.go
Torjus Håkestad 8189a108d1 feat: add shell interface, registry, and bash shell emulator
Implement Phase 1.4: replaces the hardcoded banner/timeout stub with a
proper shell system. Adds a Shell interface with weighted registry for
shell selection, a RecordingChannel wrapper (pass-through for now, prep
for Phase 2.3 replay), and a bash-like shell with fake filesystem,
terminal line reader, and command handling (pwd, ls, cd, cat, whoami,
hostname, id, uname, exit). Sessions now log command/output pairs to
the store and record the shell name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 20:24:48 +01:00

120 lines
2.6 KiB
Go

package bash
import (
"fmt"
"runtime"
"sort"
"strings"
)
type shellState struct {
cwd string
username string
hostname string
fs *filesystem
}
type commandResult struct {
output string
exit bool
}
func dispatch(state *shellState, line string) commandResult {
fields := strings.Fields(line)
if len(fields) == 0 {
return commandResult{}
}
cmd := fields[0]
args := fields[1:]
switch cmd {
case "pwd":
return commandResult{output: state.cwd}
case "whoami":
return commandResult{output: state.username}
case "hostname":
return commandResult{output: state.hostname}
case "id":
return cmdID(state)
case "uname":
return cmdUname(state, args)
case "ls":
return cmdLs(state, args)
case "cd":
return cmdCd(state, args)
case "cat":
return cmdCat(state, args)
case "exit", "logout":
return commandResult{exit: true}
default:
return commandResult{output: fmt.Sprintf("%s: command not found", cmd)}
}
}
func cmdID(state *shellState) commandResult {
return commandResult{
output: fmt.Sprintf("uid=0(%s) gid=0(%s) groups=0(%s)", state.username, state.username, state.username),
}
}
func cmdUname(state *shellState, args []string) commandResult {
if len(args) > 0 && args[0] == "-a" {
return commandResult{
output: fmt.Sprintf("Linux %s 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 %s GNU/Linux", state.hostname, runtime.GOARCH),
}
}
return commandResult{output: "Linux"}
}
func cmdLs(state *shellState, args []string) commandResult {
target := state.cwd
if len(args) > 0 {
target = resolvePath(state.cwd, args[0])
}
names, err := state.fs.list(target)
if err != nil {
return commandResult{output: err.Error()}
}
sort.Strings(names)
return commandResult{output: strings.Join(names, " ")}
}
func cmdCd(state *shellState, args []string) commandResult {
target := "/root"
if len(args) > 0 {
target = resolvePath(state.cwd, args[0])
}
if !state.fs.exists(target) {
return commandResult{output: fmt.Sprintf("bash: cd: %s: No such file or directory", args[0])}
}
if !state.fs.isDirectory(target) {
return commandResult{output: fmt.Sprintf("bash: cd: %s: Not a directory", args[0])}
}
state.cwd = target
return commandResult{}
}
func cmdCat(state *shellState, args []string) commandResult {
if len(args) == 0 {
return commandResult{}
}
var parts []string
for _, arg := range args {
p := resolvePath(state.cwd, arg)
content, err := state.fs.read(p)
if err != nil {
parts = append(parts, err.Error())
} else {
parts = append(parts, strings.TrimRight(content, "\n"))
}
}
return commandResult{output: strings.Join(parts, "\n")}
}