//
// gsh - Go lang based Shell
// (c) 2020 ITS more Co., Ltd.
// 2020-0807 created by SatoxITS (sato@its-more.jp)
//
package main // gsh main
// Documents: https://golang.org/pkg/
import (
"bufio"
"strings"
"strconv"
"fmt"
"os"
"time"
"syscall"
"go/types"
"go/token"
"net"
)
var VERSION = "gsh/0.0.4 (2020-0808a)"
var LINESIZE = (8*1024)
var PATHSEP = ":" // should be ";" in Windows
var PROMPT = "> "
func env(argv []string) {
env := os.Environ()
for _, v := range env {
fmt.Printf("%v\n",v)
}
}
func which(path string, show bool) (xfullpath string, itis bool){
pathenv, found := os.LookupEnv("PATH")
if found {
dirv := strings.Split(pathenv,PATHSEP)
for _, dir := range dirv {
fullpath := dir + "/" + path
fi, err := os.Stat(fullpath)
if err != nil {
fullpath = dir + "/" + path + ".go"
fi, err = os.Stat(fullpath)
}
if err == nil {
fm := fi.Mode()
if fm.IsRegular() {
if show {
fmt.Printf("%s\n",fullpath)
}
return fullpath, true
}
}
}
}
return "", false
}
func eval(argv []string, nlend bool){
var ai = 1
pfmt := "%s"
if argv[ai][0:1] == "%" {
pfmt = argv[ai]
ai = 2
}
if len(argv) <= ai {
return
}
gocode := strings.Join(argv[ai:]," ");
fset := token.NewFileSet()
rval, _ := types.Eval(fset,nil,token.NoPos,gocode)
fmt.Printf(pfmt,rval.Value)
if nlend { fmt.Printf("\n") }
}
func getval(name string) (found bool, val int) {
/* should expand the name here */
if name == "gsh.pid" {
return true, os.Getpid()
}else
if name == "gsh.ppid" {
return true, os.Getppid()
}
return false, 0
}
func echo(argv []string, nlend bool){
for ai := 1; ai < len(argv); ai++ {
if 1 < ai {
fmt.Printf(" ");
}
arg := argv[ai]
found, val := getval(arg)
if found {
fmt.Printf("%d",val)
}else{
fmt.Printf("%s",arg)
}
}
if nlend {
fmt.Printf("\n");
}
}
func resfile() string {
return "gsh.tmp"
}
//var resF *File
func resmap() {
//_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, os.ModeAppend)
// https://developpaper.com/solution-to-golang-bad-file-descriptor-problem/
_ , err := os.OpenFile(resfile(), os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
fmt.Printf("refF could not open: %s\n",err)
}else{
fmt.Printf("refF opened\n")
}
}
func excommand(gshPA syscall.ProcAttr, exec bool, argv []string) (ret int) {
fullpath, itis := which(argv[0],false)
if itis == false {
return -1
}
if 0 < strings.Index(fullpath,".go") {
nargv := argv // []string{}
gofullpath, itis := which("go",false)
if itis == false {
fmt.Printf("-- Go not found\n")
return -1
}
nargv = []string{ gofullpath, "run", fullpath }
fmt.Printf("-- %s {%s %s %s}\n",gofullpath,nargv[0],nargv[1],nargv[2])
if exec {
syscall.Exec(gofullpath,nargv,os.Environ())
}else{
pid, _ := syscall.ForkExec(gofullpath,nargv,&gshPA)
syscall.Wait4(pid,nil,0,nil);
}
}else{
if exec {
syscall.Exec(fullpath,argv,os.Environ())
}else{
pid, _ := syscall.ForkExec(fullpath,argv,&gshPA)
//fmt.Printf("[%d]\n",pid); // '&' to be background
syscall.Wait4(pid,nil,0,nil);
}
}
return 0
}
func sleep(gshPA syscall.ProcAttr, argv []string) {
if len(argv) < 2 {
fmt.Printf("Sleep 100ms, 100us, 100ns, ...\n")
return
}
duration := argv[1];
d, err := time.ParseDuration(duration)
if err != nil {
d, err = time.ParseDuration(duration+"s")
if err != nil {
fmt.Printf("duration ? %s (%s)\n",duration,err)
return
}
}
fmt.Printf("Sleep %v ns\n",duration)
time.Sleep(d)
if 0 < len(argv[2:]) {
gshellv(gshPA, argv[2:])
}
}
func repeat(gshPA syscall.ProcAttr, argv []string) {
if len(argv) < 2 {
return
}
start0 := time.Now()
for ri,_ := strconv.Atoi(argv[1]); 0 < ri; ri-- {
if 0 < len(argv[2:]) {
//start := time.Now()
gshellv(gshPA, argv[2:])
end := time.Now()
elps := end.Sub(start0);
if( 1000000000 < elps ){
fmt.Printf("(repeat#%d %v)\n",ri,elps);
}
}
}
}
func gen(gshPA syscall.ProcAttr, argv []string) {
if len(argv) < 2 {
fmt.Printf("Usage: %s N\n",argv[0])
return
}
// should br repeated by "repeat" command
count, _ := strconv.Atoi(argv[1])
fd := gshPA.Files[1] // Stdout
file := os.NewFile(fd,"internalStdOut")
fmt.Printf("-- Gen. Count=%d to [%d]\n",count,file.Fd())
//buf := []byte{}
outdata := "0123 5678 0123 5678 0123 5678 0123 5678\r"
for gi := 0; gi < count; gi++ {
file.WriteString(outdata)
}
//file.WriteString("\n")
fmt.Printf("\n(%d B)\n",count*len(outdata));
//file.Close()
}
// -s, -si, -so // bi-directional, source, sync (maybe socket)
func sconnect(gshPA syscall.ProcAttr, inTCP bool, argv []string) {
if len(argv) < 2 {
fmt.Printf("Usage: -s [host]:[port[.udp]]\n")
return
}
remote := argv[1]
if remote == ":" { remote = "0.0.0.0:9999" }
if inTCP { // TCP
dport, err := net.ResolveTCPAddr("tcp",remote);
if err != nil {
fmt.Printf("Address error: %s (%s)\n",remote,err)
return
}
conn, err := net.DialTCP("tcp",nil,dport)
if err != nil {
fmt.Printf("Connection error: %s (%s)\n",remote,err)
return
}
file, _ := conn.File();
fd := file.Fd()
fmt.Printf("Socket: connected to %s, socket[%d]\n",remote,fd)
savfd := gshPA.Files[1]
gshPA.Files[1] = fd;
gshellv(gshPA, argv[2:])
gshPA.Files[1] = savfd
file.Close()
conn.Close()
}else{
//dport, err := net.ResolveUDPAddr("udp4",remote);
dport, err := net.ResolveUDPAddr("udp",remote);
if err != nil {
fmt.Printf("Address error: %s (%s)\n",remote,err)
return
}
//conn, err := net.DialUDP("udp4",nil,dport)
conn, err := net.DialUDP("udp",nil,dport)
if err != nil {
fmt.Printf("Connection error: %s (%s)\n",remote,err)
return
}
file, _ := conn.File();
fd := file.Fd()
ar := conn.RemoteAddr()
//al := conn.LocalAddr()
fmt.Printf("Socket: connected to %s [%s], socket[%d]\n",
remote,ar.String(),fd)
savfd := gshPA.Files[1]
gshPA.Files[1] = fd;
gshellv(gshPA, argv[2:])
gshPA.Files[1] = savfd
file.Close()
conn.Close()
}
}
func saccept(gshPA syscall.ProcAttr, inTCP bool, argv []string) {
if len(argv) < 2 {
fmt.Printf("Usage: -ac [host]:[port[.udp]]\n")
return
}
local := argv[1]
if local == ":" { local = "0.0.0.0:9999" }
if inTCP { // TCP
port, err := net.ResolveTCPAddr("tcp",local);
if err != nil {
fmt.Printf("Address error: %s (%s)\n",local,err)
return
}
//fmt.Printf("Listen at %s...\n",local);
sconn, err := net.ListenTCP("tcp", port)
if err != nil {
fmt.Printf("Listen error: %s (%s)\n",local,err)
return
}
//fmt.Printf("Accepting at %s...\n",local);
aconn, err := sconn.AcceptTCP()
if err != nil {
fmt.Printf("Accept error: %s (%s)\n",local,err)
return
}
file, _ := aconn.File()
fd := file.Fd()
fmt.Printf("Accepted TCP at %s [%d]\n",local,fd)
savfd := gshPA.Files[0]
gshPA.Files[0] = fd;
gshellv(gshPA, argv[2:])
gshPA.Files[0] = savfd
sconn.Close();
aconn.Close();
file.Close();
}else{
//port, err := net.ResolveUDPAddr("udp4",local);
port, err := net.ResolveUDPAddr("udp",local);
if err != nil {
fmt.Printf("Address error: %s (%s)\n",local,err)
return
}
fmt.Printf("Listen UDP at %s...\n",local);
//uconn, err := net.ListenUDP("udp4", port)
uconn, err := net.ListenUDP("udp", port)
if err != nil {
fmt.Printf("Listen error: %s (%s)\n",local,err)
return
}
file, _ := uconn.File()
fd := file.Fd()
ar := uconn.RemoteAddr()
remote := ""
if ar != nil { remote = ar.String() }
if remote == "" { remote = "?" }
// not yet received
//fmt.Printf("Accepted at %s [%d] <- %s\n",local,fd,"")
savfd := gshPA.Files[0]
gshPA.Files[0] = fd;
savenv := gshPA.Env
gshPA.Env = append(savenv, "REMOTE_HOST="+remote)
gshellv(gshPA, argv[2:])
gshPA.Env = savenv
gshPA.Files[0] = savfd
uconn.Close();
file.Close();
}
}
// empty line command
func pwd(gshPA syscall.ProcAttr){
// execute context command, pwd + date
// context notation, representation scheme, to be resumed at re-login
cwd, _ := os.Getwd()
t := time.Now()
date := t.Format(time.UnixDate)
exe, _ := os.Executable()
host, _ := os.Hostname()
fmt.Printf("{PWD=\"%s\"",cwd)
fmt.Printf(" HOST=\"%s\"",host)
fmt.Printf(" DATE=\"%s\"",date)
fmt.Printf(" TIME=\"%s\"",t.String())
fmt.Printf(" PID=\"%d\"",os.Getpid())
fmt.Printf(" EXE=\"%s\"",exe)
fmt.Printf("}\n")
}
func gshellv(gshPA syscall.ProcAttr, argv []string) (fin bool) {
//fmt.Printf("-- gshellv((%d))\n",len(argv))
if len(argv) <= 0 {
return false
}
if false {
for ai := 0; ai < len(argv); ai++ {
fmt.Printf("[%d] %s [%d]%T\n",ai,argv[ai],len(argv[ai]),argv[ai])
}
}
cmd := argv[0]
if cmd == "-ot" {
sconnect(gshPA, true, argv)
return false;
}
if cmd == "-ou" {
sconnect(gshPA, false, argv)
return false;
}
if cmd == "-it" {
saccept(gshPA, true , argv)
return false;
}
if cmd == "-iu" {
saccept(gshPA, false, argv)
return false;
}
if cmd == "-i" || cmd == "-o" || cmd == "-a" || cmd == "-s" {
if len(argv) < 2 {
return false
}
fdix := 0;
mode := os.O_RDONLY;
if cmd == "-i" {
}
if cmd == "-o" {
fdix = 1;
mode = os.O_RDWR | os.O_CREATE;
}
if cmd == "-a" {
fdix = 1;
mode = os.O_RDWR | os.O_CREATE | os.O_APPEND;
}
f, err := os.OpenFile(argv[1], mode, 0600)
if err != nil {
fmt.Printf("%s\n",err)
return false
}
savfd := gshPA.Files[fdix]
gshPA.Files[fdix] = f.Fd()
fmt.Printf("-- Opened [%d] %s\n",f.Fd(),argv[1])
gshellv(gshPA, argv[2:])
gshPA.Files[fdix] = savfd
return false
}
if cmd == "call" {
excommand(gshPA, false,argv[1:])
return false
}
if cmd == "echo" {
echo(argv,true)
return false
}
if cmd == "env" {
env(argv)
return false
}
if cmd == "eval" {
eval(argv,true)
return false
}
if cmd == "exec" {
excommand(gshPA, true,argv[1:])
return false // should exit
}
if cmd == "exit" || cmd == "quit" {
// write Result code EXIT to 3>
return true
}
if cmd == "fork" {
// mainly for a server
return false
}
if cmd == "-gen" {
gen(gshPA, argv)
return false;
}
if cmd == "nop" {
return false
}
if cmd == "pstitle" {
// to be gsh.title
}
if cmd == "repeat" { // repeat cond command
repeat(gshPA,argv)
return false
}
if cmd == "set" { // set name ...
return false;
}
if cmd == "sleep" {
sleep(gshPA,argv)
return false;
}
if cmd == "-ver" {
fmt.Printf("%s\n",VERSION);
return false
}
if cmd == "pwh" {
pwd(gshPA);
return false
}
if cmd == "which" {
which(argv[1],true);
return false
}
excommand(gshPA, false,argv)
return false
}
func gshelll(gshPA syscall.ProcAttr, gline string) (rfin bool) {
argv := strings.Split(string(gline)," ")
fin := gshellv(gshPA,argv)
return fin
}
func tgshelll(gshPA syscall.ProcAttr, gline string) (xfin bool) {
start := time.Now()
fin := gshelll(gshPA,gline)
end := time.Now()
elps := end.Sub(start);
fmt.Printf("--(%d.%09ds)\n",elps/1000000000,elps%1000000000)
return fin
}
func ttyfile() string {
return "gsh.ttyline"
}
func ttyline() (*os.File){
file, err := os.OpenFile(ttyfile(), os.O_RDWR|os.O_CREATE|os.O_TRUNC,0600)
if err != nil {
fmt.Printf("cannot open %s (%s)\n",ttyfile(),err)
return file;
}
return file
}
func getline(hix int, with_exgetline bool, prevline string) (string) {
if( with_exgetline ){
//var xhix int64 = int64(hix); // cast
newenv := os.Environ()
newenv = append(newenv, "GSH_LINENO="+strconv.FormatInt(int64(hix),10) )
tty := ttyline()
tty.WriteString(prevline)
Pa := os.ProcAttr {
"", // start dir
newenv, //os.Environ(),
[]*os.File{os.Stdin,os.Stdout,os.Stderr,tty},
nil,
}
proc, err := os.StartProcess("gsh-getline",[]string{"getline","getline"},&Pa)
if err != nil {
fmt.Printf("Proc ERROR (%s)\n",nil)
for ; ; {
}
}
//stat, err := proc.Wait()
proc.Wait()
buff := make([]byte,LINESIZE)
count, err := tty.Read(buff)
//_, err = tty.Read(buff)
//fmt.Printf("-- getline (%d)\n",count)
if err != nil {
if ! (count == 0) { // && err.String() == "EOF" ) {
fmt.Printf("-- getline error (%s)\n",err)
}
}else{
//fmt.Printf("-- getline OK \"%s\"\n",buff)
}
tty.Close()
return string(buff[0:count])
}else{
// if isatty {
fmt.Printf("!%d",hix)
fmt.Print(PROMPT)
// }
reader := bufio.NewReaderSize(os.Stdin,LINESIZE)
line, _, _ := reader.ReadLine()
return string(line)
}
}
func main() {
gshPA := syscall.ProcAttr {
"", // the staring directory
os.Environ(), // environ[]
[]uintptr{os.Stdin.Fd(),os.Stdout.Fd(),os.Stderr.Fd()},
nil, // OS specific
}
//resmap()
_, with_exgetline := which("gsh-getline",false)
if with_exgetline == false {
fmt.Printf("Note: No gsh-getline found. Using internal getline.\n");
}
prevline := ""
for hix := 1; ; {
gline := getline(hix,with_exgetline,prevline)
fin := tgshelll(gshPA,gline)
if fin {
break;
}
if len(gline) == 0 {
pwd(gshPA);
continue;
}
prevline = gline;
hix++;
}
}
// TODO:
// - inter gsh communication, possibly running in remote hosts -- to be remote shell
// - merged histories of multiple parallel gsh sessions
// - alias as a function
// - instant alias end environ export to the permanent > ~/.gsh/gsh-alias and gsh-environ
// - retrieval PATH of files by its type
// - gsh as an IME
//---END--- (^-^)/