├── .gitignore ├── LICENSE ├── README.md ├── builtins.go ├── launch.go ├── main.go ├── preview.png └── terminal.c /.gitignore: -------------------------------------------------------------------------------- 1 | .sesh_history 2 | editor/main 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anas Khan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sesh 2 | 3 | ![](https://raw.githubusercontent.com/anaskhan96/sesh/master/preview.png?token=AWKfRu3IiZ6pI7l5w1BkShc5PbCrnqsNks5a7ub7wA%3D%3D) 4 | 5 | `sesh` is a simple (read basic), elegant shell written in Go. It supports the following: 6 | + Aliasing 7 | + Piping and I/O redirection 8 | + Arrow keys up and down for history 9 | + Tab autocompletion 10 | 11 | Apart from this, it has two custom builtins: 12 | + `walk`: walks through the directory specified as an argument recursively. Takes the current directory as input if no argument is specified. 13 | + `show`: lists the commands in the PATH having the given argument as its prefix. Lists all the commands in the PATH if no argument is specified. 14 | 15 | ### Installation 16 | 17 | ```bash 18 | go get -u github.com/anaskhan96/sesh 19 | ``` 20 | 21 | It can be run by invoking `sesh` from anywhere in the terminal. 22 | 23 | --- 24 | 25 | This project was built under the course *Unix Systems Programming* at *PES University*. 26 | -------------------------------------------------------------------------------- /builtins.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | var builtins = map[string]func([]string) int{ 12 | "exit": sesh_exit, 13 | "cd": sesh_cd, 14 | "help": sesh_help, 15 | "history": sesh_history, 16 | "walk": sesh_walk, 17 | "show": sesh_show, 18 | } 19 | 20 | func sesh_exit(args []string) int { 21 | fmt.Println("Exiting shell") 22 | return 0 23 | } 24 | 25 | func sesh_help(args []string) int { 26 | fmt.Println("sesh -- simple elegant shell by anaskhan96") 27 | return 1 28 | } 29 | 30 | func sesh_history(args []string) int { 31 | for _, i := range HISTMEM { 32 | fmt.Println(i) 33 | } 34 | return 1 35 | } 36 | 37 | func sesh_cd(args []string) int { 38 | if len(args) == 0 { 39 | fmt.Printf(ERRFORMAT, "Please provide a path to change directory to") 40 | } else if len(args) > 1 { 41 | fmt.Printf(ERRFORMAT, "Too many args for changing directory") 42 | } else { 43 | err := os.Chdir(args[0]) 44 | if err != nil { 45 | fmt.Printf(ERRFORMAT, err.Error()) 46 | return 2 47 | } 48 | wd, err := os.Getwd() 49 | wdSlice := strings.Split(wd, "/") 50 | os.Setenv("CWD", wdSlice[len(wdSlice)-1]) 51 | } 52 | return 1 53 | } 54 | 55 | func sesh_walk(args []string) int { 56 | var dir string 57 | if len(args) == 0 || args[0] == "." { 58 | dir, _ = filepath.Abs("") 59 | } else if args[0] == ".." { 60 | currDir, _ := filepath.Abs("") 61 | dir = filepath.Dir(currDir) 62 | } else { 63 | dir, _ = filepath.Abs(args[0]) 64 | } 65 | if fi, err := os.Stat(dir); err == nil { 66 | if fi.Mode().IsDir() { 67 | return traverse(dir) 68 | } 69 | fmt.Printf(ERRFORMAT, "Not a directory") 70 | return 2 71 | } 72 | fmt.Printf(ERRFORMAT, "Invalid path") 73 | return 2 74 | } 75 | 76 | func sesh_show(args []string) int { 77 | prefix := "" 78 | if len(args) > 1 { 79 | fmt.Printf(ERRFORMAT, "wrong usage of show") 80 | return 2 81 | } else if len(args) == 1 { 82 | prefix = args[0] 83 | } 84 | dirs := strings.Split(os.Getenv("PATH"), ":") 85 | commands := make([]string, 0, 10) 86 | for _, dir := range dirs { 87 | files, _ := ioutil.ReadDir(dir) 88 | for _, file := range files { 89 | if strings.HasPrefix(file.Name(), prefix) { 90 | commands = append(commands, file.Name()) 91 | } 92 | } 93 | } 94 | for _, command := range commands { 95 | fmt.Printf("%s\t", command) 96 | } 97 | fmt.Println() 98 | return 1 99 | } 100 | 101 | func traverse(dir string) int { 102 | dashes, _ := "|", filepath.Base(dir) 103 | filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 104 | name := filepath.Base(path) 105 | // TODO: don't show hidden files and directories in tree 106 | /*if (name != "." && name != "..") && name[0] == '.' { 107 | return filepath.SkipDir 108 | }*/ 109 | if info.IsDir() { 110 | dashes += "--" 111 | } 112 | fmt.Printf("%s %s\n", dashes, name) 113 | return nil 114 | }) 115 | return 1 116 | } 117 | -------------------------------------------------------------------------------- /launch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "time" 8 | ) 9 | 10 | func launch(args []string) int { 11 | commands, isBackground := make([]*exec.Cmd, 0, 5), false 12 | if args[len(args)-1] == "&" { 13 | isBackground = true 14 | } 15 | start, cmdInEnd := 0, true 16 | for i, arg := range args { 17 | if i == len(args)-1 && cmdInEnd { 18 | if len(commands) == 0 { 19 | return launchSimpleCommand(args, isBackground) 20 | } 21 | cmd := exec.Command(args[start], args[start+1:]...) 22 | commands = append(commands, cmd) 23 | } else if arg == "|" { 24 | cmd := exec.Command(args[start], args[start+1:i]...) 25 | commands = append(commands, cmd) 26 | start = i + 1 27 | } else if arg == ">" || arg == ">>" { 28 | cmd := exec.Command(args[start], args[start+1:i]...) 29 | var f *os.File 30 | if arg == ">" { 31 | f, _ = os.OpenFile(args[i+1], os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0666) 32 | } else { 33 | f, _ = os.OpenFile(args[i+1], os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) 34 | } 35 | cmd.Stdout = f 36 | commands = append(commands, cmd) 37 | cmdInEnd = false 38 | } else if arg == "<" { 39 | f, _ := os.Open(args[i+1]) 40 | if len(commands) > 0 { 41 | commands[0].Stdin = f 42 | } else { 43 | cmd := exec.Command(args[start], args[start+1:i]...) 44 | cmd.Stdin = f 45 | commands = append(commands, cmd) 46 | } 47 | cmdInEnd = false 48 | } 49 | } 50 | for i := range commands { 51 | if i != len(commands)-1 { 52 | if commands[i+1].Stdin == nil { 53 | commands[i+1].Stdin, _ = commands[i].StdoutPipe() 54 | } 55 | } else { 56 | if commands[i].Stdout == nil { 57 | commands[i].Stdout = os.Stdout 58 | } 59 | } 60 | } 61 | for i := len(commands) - 1; i > 0; i-- { 62 | commands[i].Start() 63 | } 64 | timestamp := time.Now().String() 65 | if !isBackground { 66 | if err := commands[0].Run(); err != nil { 67 | fmt.Printf(ERRFORMAT, err.Error()) 68 | return 2 69 | } 70 | } else { 71 | commands[0].Start() 72 | } 73 | for i := range commands[1:] { 74 | commands[i].Wait() 75 | } 76 | HISTLINE = fmt.Sprintf("%d::%s::%s", commands[0].Process.Pid, timestamp, HISTLINE) 77 | return 1 78 | } 79 | 80 | func launchSimpleCommand(args []string, isBackground bool) int { 81 | // Spawning and executing a process 82 | cmd := exec.Command(args[0], args[1:]...) 83 | // Setting stdin, stdout, and stderr 84 | cmd.Stdin = os.Stdin 85 | cmd.Stdout = os.Stdout 86 | cmd.Stderr = os.Stderr 87 | cmd.Env = nil // making sure the command uses the current process' environment 88 | timestamp := time.Now().String() 89 | if !isBackground { 90 | if err := cmd.Run(); err != nil { 91 | fmt.Printf(ERRFORMAT, err.Error()) 92 | return 2 93 | } 94 | } else { 95 | cmd.Start() 96 | } 97 | HISTLINE = fmt.Sprintf("%d::%s::%s", cmd.Process.Pid, timestamp, HISTLINE) 98 | return 1 99 | } 100 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | extern void disableRawMode(); 5 | extern void enableRawMode(); 6 | */ 7 | import "C" 8 | 9 | import ( 10 | "bufio" 11 | "fmt" 12 | "io/ioutil" 13 | "os" 14 | "path/filepath" 15 | "regexp" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | const ( 21 | TOKDELIM = " \t\r\n\a" 22 | ERRFORMAT = "sesh: %s\n" 23 | ) 24 | 25 | var ( 26 | HISTSIZE = 25 27 | HISTFILE string 28 | HISTMEM []string 29 | HISTCOUNT int 30 | HISTLINE string 31 | CONFIG string 32 | aliases map[string]string 33 | ) 34 | 35 | func main() { 36 | sesh_setup() 37 | 38 | sesh_loop() 39 | 40 | exit() 41 | } 42 | 43 | func sesh_setup() { 44 | wd, err := os.Getwd() 45 | if err != nil { 46 | fmt.Printf(ERRFORMAT, err.Error()) 47 | } 48 | wdSlice := strings.Split(wd, "/") 49 | os.Setenv("CWD", wdSlice[len(wdSlice)-1]) 50 | 51 | HISTFILE = fmt.Sprintf("%s/%s", os.Getenv("HOME"), ".sesh_history") 52 | 53 | /* Importing config */ 54 | sesh_config() 55 | } 56 | 57 | func sesh_config() { 58 | CONFIG = fmt.Sprintf("%s/%s", os.Getenv("HOME"), ".seshrc") 59 | aliases = make(map[string]string) 60 | if _, err := os.Stat(CONFIG); err == nil { 61 | f, _ := os.OpenFile(CONFIG, os.O_RDONLY, 0666) 62 | defer f.Close() 63 | scanner := bufio.NewScanner(f) 64 | for scanner.Scan() { 65 | parseLine(scanner.Text()) 66 | } 67 | } 68 | } 69 | 70 | func sesh_loop() { 71 | HISTMEM = initHistory(HISTMEM) 72 | status := 1 73 | reader := bufio.NewReader(os.Stdin) 74 | 75 | for status != 0 { 76 | C.enableRawMode() 77 | symbol := "\u2713" 78 | if status == 2 { 79 | symbol = "\u2715" 80 | } 81 | fmt.Printf("\033[36msesh 🔥 \033[33m%s \033[36m%s \033[m", os.Getenv("CWD"), symbol) 82 | line, discard, cursorPos, histCounter, shellEditor := "", false, 0, 0, false 83 | for { 84 | c, _ := reader.ReadByte() 85 | if shellEditor && c == 13 { 86 | line = line[:len(line)-1] 87 | fmt.Println() 88 | shellEditor = false 89 | cursorPos = len(line) 90 | continue 91 | } 92 | shellEditor = false 93 | if c == 27 { 94 | c1, _ := reader.ReadByte() 95 | if c1 == '[' { 96 | c2, _ := reader.ReadByte() 97 | switch c2 { 98 | case 'A': 99 | if len(HISTMEM) != 0 && histCounter < len(HISTMEM) { 100 | for cursorPos > 0 { 101 | fmt.Printf("\b\033[J") 102 | cursorPos-- 103 | } 104 | line = strings.Split(HISTMEM[histCounter], "::")[2] 105 | fmt.Printf(line) 106 | cursorPos = len(line) 107 | histCounter++ 108 | } 109 | case 'B': 110 | if len(HISTMEM) != 0 && histCounter > 0 { 111 | for cursorPos > 0 { 112 | fmt.Printf("\b\033[J") 113 | cursorPos-- 114 | } 115 | histCounter-- 116 | line = strings.Split(HISTMEM[histCounter], "::")[2] 117 | fmt.Printf(line) 118 | cursorPos = len(line) 119 | } 120 | case 'C': 121 | if cursorPos < len(line) { 122 | fmt.Printf("\033[C") 123 | cursorPos++ 124 | } 125 | case 'D': 126 | if cursorPos > 0 { 127 | fmt.Printf("\033[D") 128 | cursorPos-- 129 | } 130 | } 131 | } 132 | continue 133 | } 134 | // backspace was pressed 135 | if c == 127 { 136 | if cursorPos > 0 { 137 | if cursorPos != len(line) { 138 | temp, oldLength := line[cursorPos:], len(line) 139 | fmt.Printf("\b\033[K%s", temp) 140 | for oldLength != cursorPos { 141 | fmt.Printf("\033[D") 142 | oldLength-- 143 | } 144 | line = line[:cursorPos-1] + temp 145 | cursorPos-- 146 | } else { 147 | fmt.Print("\b\033[K") 148 | line = line[:len(line)-1] 149 | cursorPos-- 150 | } 151 | } 152 | continue 153 | } 154 | // ctrl-c was pressed 155 | if c == 3 { 156 | fmt.Println("^C") 157 | discard = true 158 | break 159 | } 160 | // ctrl-d was pressed 161 | if c == 4 { 162 | exit() 163 | } 164 | // the enter key was pressed 165 | if c == 13 { 166 | fmt.Println() 167 | break 168 | } 169 | // tab was pressed 170 | if c == 9 { 171 | args := strings.Fields(line) 172 | if len(line) > 1 { 173 | arg := args[len(args)-1] 174 | pattern, dir := arg, "." 175 | if strings.Contains(arg, "/") { 176 | pattern, dir = filepath.Base(arg), filepath.Dir(arg) 177 | } 178 | files, _ := ioutil.ReadDir(dir) 179 | matches := make([]string, 0, 10) 180 | for _, file := range files { 181 | if strings.HasPrefix(file.Name(), pattern) { 182 | matches = append(matches, file.Name()) 183 | } 184 | } 185 | if len(matches) == 1 { 186 | pathToAppend := matches[0] 187 | if strings.Contains(arg, "/") { 188 | pathToAppend = fmt.Sprintf("%s/%s", dir, matches[0]) 189 | } 190 | args[len(args)-1] = pathToAppend 191 | line = strings.Join(args, " ") 192 | for cursorPos > 0 { 193 | fmt.Printf("\b\033[K") 194 | cursorPos-- 195 | } 196 | fmt.Printf("%s", line) 197 | cursorPos = len(line) 198 | continue 199 | } 200 | } 201 | continue 202 | } 203 | if cursorPos == len(line) { 204 | fmt.Printf("%c", c) 205 | line += string(c) 206 | cursorPos = len(line) 207 | } else { 208 | temp, oldLength := line[cursorPos:], len(line) 209 | fmt.Printf("\033[K%c%s", c, temp) 210 | for oldLength != cursorPos { 211 | fmt.Printf("\033[D") 212 | oldLength-- 213 | } 214 | line = line[:cursorPos] + string(c) + temp 215 | cursorPos++ 216 | } 217 | if c == '\\' { 218 | shellEditor = true 219 | } 220 | } 221 | C.disableRawMode() 222 | if line == "" || discard { 223 | status = 1 224 | continue 225 | } 226 | HISTLINE, status = line, 1 227 | line = strings.Replace(line, "~", os.Getenv("HOME"), -1) 228 | args, ok := parseLine(line) 229 | if ok && args != nil { 230 | status = execute(args) 231 | } else { 232 | status = 2 233 | } 234 | if status == 1 { 235 | /* Store line in history */ 236 | if HISTCOUNT == HISTSIZE { 237 | HISTMEM = HISTMEM[1:] 238 | HISTCOUNT = 0 239 | } 240 | HISTMEM = append([]string{HISTLINE}, HISTMEM...) 241 | HISTCOUNT++ 242 | } 243 | } 244 | } 245 | 246 | func parseLine(line string) ([]string, bool) { 247 | args := regexp.MustCompile("'(.+)'|\"(.+)\"|\\S+").FindAllString(line, -1) 248 | for i, arg := range args { 249 | if (arg[0] == '"' && arg[len(arg)-1] == '"') || (arg[0] == '\'' && arg[len(arg)-1] == '\'') { 250 | args[i] = arg[1 : len(arg)-1] 251 | } 252 | } 253 | if args[0] == "alias" { 254 | if len(args) == 1 { 255 | fmt.Printf(ERRFORMAT, "arguments needed for alias") 256 | return nil, false 257 | } 258 | for _, i := range args[1:] { 259 | aliasArgs := strings.Split(i, "=") 260 | if len(aliasArgs) != 2 { 261 | fmt.Printf(ERRFORMAT, "wrong format of alias") 262 | return nil, false 263 | } 264 | aliases[aliasArgs[0]] = aliasArgs[1] 265 | } 266 | return args, false 267 | } 268 | if args[0] == "export" { 269 | if len(args) == 1 { 270 | fmt.Printf(ERRFORMAT, "argument needed for export") 271 | return nil, false 272 | } 273 | exportArgs := strings.Split(args[1], "=") 274 | if len(exportArgs) != 2 { 275 | fmt.Printf(ERRFORMAT, "wrong format of export") 276 | return nil, false 277 | } 278 | os.Setenv(exportArgs[0], exportArgs[1]) 279 | return args, false 280 | } 281 | // replace if an alias 282 | for i, arg := range args { 283 | if val, ok := aliases[arg]; ok { 284 | args[i] = val 285 | } 286 | } 287 | // replace if an environment variable 288 | for i, arg := range args { 289 | if arg[0] == '$' { 290 | args[i] = os.Getenv(arg[1:]) 291 | } 292 | } 293 | // wildcard support (not really efficient) 294 | wildcardArgs := make([]string, 0, 5) 295 | for _, arg := range args { 296 | if strings.Contains(arg, "*") || strings.Contains(arg, "?") { 297 | matches, _ := filepath.Glob(arg) 298 | wildcardArgs = append(wildcardArgs, matches...) 299 | } else { 300 | wildcardArgs = append(wildcardArgs, arg) 301 | } 302 | } 303 | args = wildcardArgs 304 | return args, true 305 | } 306 | 307 | func execute(args []string) int { 308 | if len(args) == 0 { 309 | return 1 310 | } 311 | for k, v := range builtins { 312 | if args[0] == k { 313 | timestamp := time.Now().String() 314 | HISTLINE = fmt.Sprintf("%d::%s::%s", os.Getpid(), timestamp, HISTLINE) 315 | return v(args[1:]) 316 | } 317 | } 318 | return launch(args) 319 | } 320 | 321 | func initHistory(history []string) []string { 322 | if _, err := os.Stat(HISTFILE); err == nil { 323 | f, _ := os.OpenFile(HISTFILE, os.O_RDONLY, 0666) 324 | defer f.Close() 325 | /* Read file and store each line in history slice */ 326 | scanner := bufio.NewScanner(f) 327 | for scanner.Scan() { 328 | history = append(history, scanner.Text()) 329 | HISTCOUNT++ 330 | } 331 | } 332 | return history 333 | } 334 | 335 | func exit() { 336 | f, err := os.OpenFile(HISTFILE, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666) 337 | if err != nil { 338 | fmt.Printf(ERRFORMAT, err.Error()) 339 | } 340 | defer f.Close() 341 | for _, i := range HISTMEM { 342 | f.Write([]byte(i)) 343 | f.Write([]byte("\n")) 344 | } 345 | os.Exit(0) 346 | } 347 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anaskhan96/sesh/6dafb28804929a09a8b11718b4b8ecfd09594a0d/preview.png -------------------------------------------------------------------------------- /terminal.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct termios orig_termios; 6 | 7 | void disableRawMode() { 8 | tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); 9 | } 10 | 11 | void enableRawMode() { 12 | tcgetattr(STDIN_FILENO, &orig_termios); 13 | struct termios raw = orig_termios; 14 | raw.c_iflag &= ~(ICRNL | IXON); 15 | //raw.c_oflag &= ~(OPOST); 16 | raw.c_lflag &= ~(ECHO | IEXTEN | ICANON | ISIG); 17 | tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); 18 | } --------------------------------------------------------------------------------