RegMs If 4 лет назад
Родитель
Сommit
beafbc4fca
17 измененных файлов с 1108 добавлено и 0 удалено
  1. 42 0
      cmd_bg.go
  2. 30 0
      cmd_cd.go
  3. 42 0
      cmd_dir.go
  4. 17 0
      cmd_echo.go
  5. 17 0
      cmd_env.go
  6. 23 0
      cmd_exit.go
  7. 45 0
      cmd_fg.go
  8. 29 0
      cmd_jobs.go
  9. 30 0
      cmd_set.go
  10. 45 0
      cmd_shift.go
  11. 70 0
      cmd_tests.go
  12. 27 0
      cmd_umask.go
  13. 24 0
      cmd_unset.go
  14. 3 0
      go.mod
  15. 111 0
      manual.go
  16. 101 0
      manual.txt
  17. 452 0
      myshell.go

+ 42 - 0
cmd_bg.go

@@ -0,0 +1,42 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"syscall"
+)
+
+// 后台执行job
+func cmd_bg(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) == 1 { // 如果没有指定job,则选编号最小的
+		if len(jobs) == 0 { // 如果没有job
+			panic("bg: no current job") // 则提示错误
+		}
+
+		for i := range jobs {
+			fields = append(fields, "%"+strconv.Itoa(i))
+			break
+		}
+	}
+
+	for _, jid := range fields[1:] { // 遍历参数
+		id, err := strconv.Atoi(jid[1:]) // 字符串转数字
+		if err != nil {
+			panic("bg: job not found: " + jid)
+		}
+
+		_, ok := jobs[id] // 是否有指定编号的job
+		if !ok {
+			panic("bg: %" + jid[1:] + ": no such job")
+		}
+
+		if jobs[id].state == "running" { // job已经在后台执行
+			panic("bg: job already in background")
+		}
+		jobs[id].state = "running"                             // 修改状态
+		jobs[id].cmd.Process.Signal(syscall.SIGCONT)           // 向进程发送SIGCONT信号,继续执行
+		os.Setenv("!", strconv.Itoa(jobs[id].cmd.Process.Pid)) // 更新最近一次后台进程的pid
+		fmt.Fprintf(stdout, "[%d]    %d continued\t%s\n", id, jobs[id].cmd.Process.Pid, jobs[id].work)
+	}
+}

+ 30 - 0
cmd_cd.go

@@ -0,0 +1,30 @@
+package main
+
+import (
+	"os"
+)
+
+// 修改工作目录
+func cmd_cd(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) > 2 { // 最多1个参数
+		panic("cd: too many arguments")
+	}
+
+	if len(fields) == 1 { // 如果没有指定目录,则切换到home
+		fields = append(fields, os.Getenv("HOME"))
+	}
+	if fields[1] == "~" { // 如果指定目录为~,则切换到home
+		fields[1] = os.Getenv("HOME")
+	}
+
+	err := os.Chdir(fields[1]) // 切换目录
+	if err != nil {
+		panic(err)
+	}
+
+	dir, err := os.Getwd() // 获取工作目录
+	if err != nil {
+		panic(err)
+	}
+	os.Setenv("PWD", dir) // 更新pwd环境变量
+}

+ 42 - 0
cmd_dir.go

@@ -0,0 +1,42 @@
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+// 显示目录内容
+func cmd_dir(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) == 1 { // 如果没有指定目录,则显示当前目录
+		fields = []string{fields[0], "."}
+	}
+
+	for i, dir := range fields[1:] { // 遍历参数
+		files, err := os.ReadDir(dir) // 获取目录内容
+		if err != nil {
+			panic(err)
+		}
+
+		if len(fields) > 2 { // 如果要显示多个目录
+			if i > 0 {
+				fmt.Fprintln(stdout)
+			}
+			fmt.Fprintf(stdout, "%s:\n", dir) // 则输出提示
+		}
+
+		for _, file := range files { // 遍历文件
+			info, err := file.Info() // 获取文件信息
+			if err != nil {
+				panic(err)
+			}
+
+			if info.IsDir() { // 根据文件信息显示不同颜色
+				fmt.Fprintf(stdout, "\033[94m%s\033[0m\n", file.Name())
+			} else if info.Mode()&0111 > 0 {
+				fmt.Fprintf(stdout, "\033[92m%s\033[0m\n", file.Name())
+			} else {
+				fmt.Fprintln(stdout, file.Name())
+			}
+		}
+	}
+}

+ 17 - 0
cmd_echo.go

@@ -0,0 +1,17 @@
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+// 显示内容并换行
+func cmd_echo(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	for i, str := range fields[1:] { // 遍历参数
+		if i > 0 {
+			fmt.Fprint(stdout, " ")
+		}
+		fmt.Fprint(stdout, os.ExpandEnv(str)) // 替换环境变量后输出
+	}
+	fmt.Fprintln(stdout)
+}

+ 17 - 0
cmd_env.go

@@ -0,0 +1,17 @@
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+// 显示用户环境变量
+func cmd_env(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) > 1 { // 不能有参数
+		panic("env: too many arguments")
+	}
+
+	for _, env := range userenv { // 遍历用户环境变量
+		fmt.Fprintln(stdout, env) // 并输出
+	}
+}

+ 23 - 0
cmd_exit.go

@@ -0,0 +1,23 @@
+package main
+
+import (
+	"os"
+	"strconv"
+)
+
+// 退出shell
+func cmd_exit(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) > 2 { // 最多1个参数
+		panic("exit: too many arguments")
+	}
+
+	if len(fields) == 1 { // 如果没有指定退出状态
+		fields = append(fields, "0") // 则默认为0
+	}
+
+	code, err := strconv.Atoi(fields[1]) // 字符串转数字
+	if err != nil {
+		panic("exit: " + fields[1] + ": numeric argument required")
+	}
+	os.Exit(code) // 退出shell
+}

+ 45 - 0
cmd_fg.go

@@ -0,0 +1,45 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"syscall"
+)
+
+// 前台执行job
+func cmd_fg(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) == 1 { // 如果没有指定job,则选编号最小的
+		if len(jobs) == 0 { // 如果没有job
+			panic("fg: no current job") // 则提示错误
+		}
+
+		for i := range jobs {
+			fields = append(fields, "%"+strconv.Itoa(i))
+			break
+		}
+	}
+
+	for _, jid := range fields[1:] { // 遍历参数
+		id, err := strconv.Atoi(jid[1:]) // 字符串转数字
+		if err != nil {
+			panic("fg: job not found: " + jid)
+		}
+
+		_, ok := jobs[id] // 是否有指定编号的job
+		if !ok {
+			panic("fg: %" + jid[1:] + ": no such job")
+		}
+
+		cur = job{jobs[id].wait, "running", jobs[id].work, jobs[id].cmd} // 前台执行
+		delete(jobs, id)                                                 // 删除job
+		cur.cmd.Process.Signal(syscall.SIGCONT)                          // 向进程发送SIGCONT信号,继续执行
+		fmt.Fprintf(stdout, "[%d]    %d continued\t%s\n", id, cur.cmd.Process.Pid, cur.work)
+
+		term = make(chan bool) // 初始化停止信号管道
+
+		<-term        // 等待接收信号,实现前台执行
+		close(term)   // 关闭停止管道
+		cur.cmd = nil // 前台没有在执行命令
+	}
+}

+ 29 - 0
cmd_jobs.go

@@ -0,0 +1,29 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+)
+
+// 显示job列表
+func cmd_jobs(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) == 1 { // 如果没有参数
+		for i, job := range jobs { // 则输出所有job
+			fmt.Fprintf(stdout, "[%d]    %d %s\t%s\n", i, job.cmd.Process.Pid, job.state, job.work)
+		}
+	} else {
+		for _, jid := range fields[1:] { // 遍历参数
+			id, err := strconv.Atoi(jid[1:]) // 字符串转数字
+			if err != nil {
+				panic("jobs: job not found: " + jid)
+			}
+
+			_, ok := jobs[id] // 是否有指定编号的job
+			if !ok {
+				panic("jobs: %" + jid[1:] + ": no such job")
+			}
+			fmt.Fprintf(stdout, "[%d]    %d %s\t%s\n", id, jobs[id].cmd.Process.Pid, jobs[id].state, jobs[id].work)
+		}
+	}
+}

+ 30 - 0
cmd_set.go

@@ -0,0 +1,30 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+)
+
+// 显示所有环境变量或设置命令行参数
+func cmd_set(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) == 1 { // 如果没有参数
+		for _, env := range os.Environ() { // 则输出所有用户和shell环境变量
+			fmt.Fprintln(stdout, env)
+		}
+	} else {
+		tot, _ := strconv.Atoi(os.Getenv("#")) // 获取参数个数
+
+		for i := 1; i <= tot; i++ { // 重设所有参数
+			os.Unsetenv(strconv.Itoa(i))
+		}
+
+		for i := 1; i < len(fields); i++ { // 设置新参数
+			os.Setenv(strconv.Itoa(i), fields[i])
+		}
+
+		os.Setenv("#", strconv.Itoa(len(fields)-1))   // 更新参数个数
+		os.Setenv("*", strings.Join(fields[1:], " ")) // 更新所有命令行参数的值
+	}
+}

+ 45 - 0
cmd_shift.go

@@ -0,0 +1,45 @@
+package main
+
+import (
+	"os"
+	"strconv"
+	"strings"
+)
+
+// 左移参数
+func cmd_shift(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) > 2 { // 最多1个参数
+		panic("shift: too many arguments")
+	}
+
+	if len(fields) == 1 { // 如果没有指定左移几次
+		fields = append(fields, "1") // 则默认为1
+	}
+
+	num, err := strconv.Atoi(fields[1]) // 字符串转数字
+	if err != nil {
+		panic("shift: " + fields[1] + ": numeric argument required")
+	}
+	if num < 0 {
+		panic("shift: argument to shift must be non-negative")
+	}
+
+	tot, _ := strconv.Atoi(os.Getenv("#")) // 获取参数个数
+	if num > tot {
+		panic("shift: shift count must be <= $#")
+	}
+
+	args := []string{} // 参数数组
+
+	for i := 1; i <= tot-num; i++ { // 设置参数
+		args = append(args, os.Getenv(strconv.Itoa(i+num)))
+		os.Setenv(strconv.Itoa(i), args[len(args)-1])
+	}
+
+	for i := tot - num + 1; i <= tot; i++ { // 重设参数
+		os.Unsetenv(strconv.Itoa(i))
+	}
+
+	os.Setenv("#", strconv.Itoa(tot-num))   // 更新参数个数
+	os.Setenv("*", strings.Join(args, " ")) // 更新所有命令行参数的值
+}

+ 70 - 0
cmd_tests.go

@@ -0,0 +1,70 @@
+package main
+
+import (
+	"os"
+	"strconv"
+)
+
+// 测试条件
+func cmd_test(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	res := true // 测试结果
+
+	if len(fields) == 3 { // 2个参数,默认为测试文件
+		info, err := os.Stat(fields[2]) // 获取文件状态
+		if err != nil {
+			res = false
+		} else {
+			switch fields[1] {
+			case "-d": // 是否为目录文件
+				res = info.Mode().IsDir()
+			case "-f": // 是否为普通文件
+				res = info.Mode().IsRegular()
+			case "-s": // 是否大小不为0
+				res = info.Size() > 0
+			case "-r": // 是否可读
+				res = info.Mode()&0444 > 0
+			case "-w": // 是否可写
+				res = info.Mode()&0222 > 0
+			case "-x": // 是否可执行
+				res = info.Mode()&0111 > 0
+			default:
+				res = false
+			}
+		}
+	} else if len(fields) == 4 { // 3个参数,默认为测试数字
+		a, err := strconv.Atoi(os.ExpandEnv(fields[1])) // 字符串转数字
+		if err != nil {
+			panic("test: integer expression expected: " + os.ExpandEnv(fields[1]))
+		}
+
+		b, err := strconv.Atoi(os.ExpandEnv(fields[3])) // 字符串转数字
+		if err != nil {
+			panic("test: integer expression expected: " + os.ExpandEnv(fields[3]))
+		}
+
+		switch fields[2] {
+		case "-eq": // 是否相等
+			res = a == b
+		case "-ge": // 是否大于等于
+			res = a >= b
+		case "-gt": // 是否大于
+			res = a > b
+		case "-le": // 是否小于等于
+			res = a <= b
+		case "-lt": // 是否小于
+			res = a < b
+		case "-ne": // 是否不等于
+			res = a != b
+		case "==": // 是否等于
+			res = a == b
+		case "!=": // 是否不等于
+			res = a != b
+		default:
+			res = false
+		}
+	}
+
+	if !res { // 如果条件为假,则退出状态不为0
+		panic("")
+	}
+}

+ 27 - 0
cmd_umask.go

@@ -0,0 +1,27 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"syscall"
+)
+
+// 显示或设置umask
+func cmd_umask(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) > 2 { // 最多1个参数
+		panic("umask: too many arguments")
+	}
+
+	if len(fields) == 1 { // 如果没有参数
+		old := syscall.Umask(022)          // 修改并保存旧umask
+		syscall.Umask(old)                 // 修改成旧umask,保持不变
+		fmt.Fprintf(stdout, "%04o\n", old) // 输出umask
+	} else {
+		new, err := strconv.ParseInt(fields[1], 8, 0) // 字符串转8进制数
+		if err != nil {
+			panic("umask: " + fields[1] + ": numeric argument required")
+		}
+		syscall.Umask(int(new)) // 修改umask
+	}
+}

+ 24 - 0
cmd_unset.go

@@ -0,0 +1,24 @@
+package main
+
+import (
+	"os"
+	"regexp"
+)
+
+// 重设变量
+func cmd_unset(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) == 1 { // 至少1个参数
+		panic("unset: not enough arguments")
+	}
+
+	for _, str := range fields[1:] { // 遍历参数
+		matched, err := regexp.MatchString("^[A-Za-z_][0-9A-Za-z_]*", str) // 是否满足用户定义变量名规则
+		if err != nil {
+			panic(err)
+		}
+		if !matched {
+			panic("unset: '" + str + "': not a valid identifier")
+		}
+		os.Unsetenv(str)
+	}
+}

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module myshell
+
+go 1.16

+ 111 - 0
manual.go

@@ -0,0 +1,111 @@
+package main
+
+import (
+	"io"
+	"strings"
+)
+
+func manual() io.Reader {
+	return strings.NewReader(`bg :后台执行job
+- bg [%JID]... :使指定编号的job在后台继续执行
+- bg :使编号最小的job在后台继续执行
+
+[前台执行的进程 :在shell中执行的进程;在进程结束并返回shell之前,shell不能执行其他命令]
+[后台执行的进程 :shell的子进程,隐式执行;在进程结束并返回shell之前,shell可以执行其他命令]
+
+cd :修改工作目录
+- cd DIRECTORY :将工作目录修改为DIRECTORY
+- cd .. :返回上一层目录
+- cd :将工作目录修改为主目录
+- cd ~ :将工作目录修改为主目录
+
+clr :清屏
+- clr :清屏并在屏幕第一行显示命令提示符
+
+dir :显示目录内容
+- dir :显示工作目录(即当前目录)下的所有文件
+- dir DIRECTORY :显示DIRECTORY下的所有文件
+
+echo :显示内容并换行
+- echo COMMENT :在屏幕上显示COMMENT并换行
+
+env :显示用户环境变量
+- env :显示所有用户环境变量
+
+[环境变量 :指定操作系统运行环境的一些参数,保证shell命令的正确执行]
+[用户环境变量 :与shell无关的环境变量,不同的shell拥有相同的用户环境变量]
+
+exec :执行命令
+- exec COMMAND :执行COMMAND命令
+
+exit :退出shell
+- exit :退出当前shell,退出状态为0
+- exit CODE :退出当前shell,退出状态为CODE
+
+fg :前台执行job
+- fg [%JID]... :使指定编号的job在前台继续执行
+- fg :使编号最小的job在前台继续执行
+
+help :显示用户手册
+- help :显示用户手册,并用more过滤
+- help -N :显示用户手册,并用more过滤,每页显示N行
+- help +N :显示用户手册,并用more过滤,从第N行开始显示
+
+jobs :显示job列表
+- jobs :显示所有job的列表
+- jobs [%JID]... :显示指定编号的job的列表
+
+pwd :显示工作目录
+- pwd :显示工作目录绝对路径
+
+set :显示所有环境变量或设置参数
+- set :显示所有环境变量(包括用户环境变量和shell环境变量)
+- set [STR]... :设置命令行参数为指定列表
+
+[shell环境变量 :与shell有关的环境变量,不同的shell拥有不同的shell环境变量]
+[同一种shell的不同进程也拥有不同的shell环境变量]
+
+shift :左移参数
+- shift :将命令行参数左移1位
+- shift [NUM] :将命令行参数左移NUM位
+
+test :测试条件
+- test -d FILE :测试FILE是否为目录文件
+- test -f FILE :测试FILE是否为普通文件
+- test -s FILE :测试FILE大小是否不为0
+- test -r FILE :测试FILE是否可读
+- test -w FILE :测试FILE是否可写
+- test -x FILE :测试FILE是否可执行
+- test NUM1 -eq NUM2 :测试NUM1是否等于NUM2
+- test NUM1 -ge NUM2 :测试NUM1是否大于等于NUM2
+- test NUM1 -gt NUM2 :测试NUM1是否大于NUM2
+- test NUM1 -le NUM2 :测试NUM1是否小于等于NUM2
+- test NUM1 -lt NUM2 :测试NUM1是否小于NUM2
+- test NUM1 -ne NUM2 :测试NUM1是否不等于NUM2
+- test NUM1 == NUM2 :测试NUM1是否等于NUM2
+- test NUM1 != NUM2 :测试NUM1是否不等于NUM2
+
+time :显示时间
+- time :显示当前时间
+
+umask :显示或设置umask
+- umask :显示当前umask
+- umask OCT :设置umask为OCT
+
+[umask :用于设置用户创建文件和目录的默认权限;umask用3位8进制数表示,分别为所有者、所在组、其他的权限,每1位8进制表示3位2进制,分别为读、写、执行权限]
+[目录文件创建时的默认权限为0777&~umask,即umask中为1的位在默认权限中为0;例如umask=0022时,目录文件的默认权限为0755]
+[普通文件创建时的默认权限为0666&~umask;例如umask=0022时,普通文件的默认权限为0644]
+
+unset :重设变量
+- unset [VAR]... :重设指定的环境变量,值变为空
+
+[I/O重定向 :在命令中使用< FILE可以将FILE文件作为命令的标准输入]
+[在命令中使用> FILE可以将FILE文件作为命令的标准输出,如果FILE文件已存在则会覆盖写入]
+[在命令中使用>> FILE可以将FILE文件作为命令的标准输出,如果FILE文件已存在则会追加写入]
+[在命令中使用2> FILE可以将FILE文件作为命令的标准错误输出,如果FILE文件已存在则会覆盖写入]
+[在命令中使用2>> FILE可以将FILE文件作为命令的标准错误输出,如果FILE文件已存在则会覆盖写入]
+
+[管道 :执行命令COMMAND1 | COMMAND2 | ... | COMMANDN,可以依次将前一个命令的标准输出作为下一个命令的标准输入]
+[第一个命令的标准输入和最后一个命令的标准输出为默认值]
+	`)
+}

+ 101 - 0
manual.txt

@@ -0,0 +1,101 @@
+bg :后台执行job
+- bg [%JID]... :使指定编号的job在后台继续执行
+- bg :使编号最小的job在后台继续执行
+
+[前台执行的进程 :在shell中执行的进程;在进程结束并返回shell之前,shell不能执行其他命令]
+[后台执行的进程 :shell的子进程,隐式执行;在进程结束并返回shell之前,shell可以执行其他命令]
+
+cd :修改工作目录
+- cd DIRECTORY :将工作目录修改为DIRECTORY
+- cd .. :返回上一层目录
+- cd :将工作目录修改为主目录
+- cd ~ :将工作目录修改为主目录
+
+clr :清屏
+- clr :清屏并在屏幕第一行显示命令提示符
+
+dir :显示目录内容
+- dir :显示工作目录(即当前目录)下的所有文件
+- dir DIRECTORY :显示DIRECTORY下的所有文件
+
+echo :显示内容并换行
+- echo COMMENT :在屏幕上显示COMMENT并换行
+
+env :显示用户环境变量
+- env :显示所有用户环境变量
+
+[环境变量 :指定操作系统运行环境的一些参数,保证shell命令的正确执行]
+[用户环境变量 :与shell无关的环境变量,不同的shell拥有相同的用户环境变量]
+
+exec :执行命令
+- exec COMMAND :执行COMMAND命令
+
+exit :退出shell
+- exit :退出当前shell,退出状态为0
+- exit CODE :退出当前shell,退出状态为CODE
+
+fg :前台执行job
+- fg [%JID]... :使指定编号的job在前台继续执行
+- fg :使编号最小的job在前台继续执行
+
+help :显示用户手册
+- help :显示用户手册,并用more过滤
+- help -N :显示用户手册,并用more过滤,每页显示N行
+- help +N :显示用户手册,并用more过滤,从第N行开始显示
+
+jobs :显示job列表
+- jobs :显示所有job的列表
+- jobs [%JID]... :显示指定编号的job的列表
+
+pwd :显示工作目录
+- pwd :显示工作目录绝对路径
+
+set :显示所有环境变量或设置参数
+- set :显示所有环境变量(包括用户环境变量和shell环境变量)
+- set [STR]... :设置命令行参数为指定列表
+
+[shell环境变量 :与shell有关的环境变量,不同的shell拥有不同的shell环境变量]
+[同一种shell的不同进程也拥有不同的shell环境变量]
+
+shift :左移参数
+- shift :将命令行参数左移1位
+- shift [NUM] :将命令行参数左移NUM位
+
+test :测试条件
+- test -d FILE :测试FILE是否为目录文件
+- test -f FILE :测试FILE是否为普通文件
+- test -s FILE :测试FILE大小是否不为0
+- test -r FILE :测试FILE是否可读
+- test -w FILE :测试FILE是否可写
+- test -x FILE :测试FILE是否可执行
+- test NUM1 -eq NUM2 :测试NUM1是否等于NUM2
+- test NUM1 -ge NUM2 :测试NUM1是否大于等于NUM2
+- test NUM1 -gt NUM2 :测试NUM1是否大于NUM2
+- test NUM1 -le NUM2 :测试NUM1是否小于等于NUM2
+- test NUM1 -lt NUM2 :测试NUM1是否小于NUM2
+- test NUM1 -ne NUM2 :测试NUM1是否不等于NUM2
+- test NUM1 == NUM2 :测试NUM1是否等于NUM2
+- test NUM1 != NUM2 :测试NUM1是否不等于NUM2
+
+time :显示时间
+- time :显示当前时间
+
+umask :显示或设置umask
+- umask :显示当前umask
+- umask OCT :设置umask为OCT
+
+[umask :用于设置用户创建文件和目录的默认权限;umask用3位8进制数表示,分别为所有者、所在组、其他的权限,每1位8进制表示3位2进制,分别为读、写、执行权限]
+[目录文件创建时的默认权限为0777&~umask,即umask中为1的位在默认权限中为0;例如umask=0022时,目录文件的默认权限为0755]
+[普通文件创建时的默认权限为0666&~umask;例如umask=0022时,普通文件的默认权限为0644]
+
+unset :重设变量
+- unset [VAR]... :重设指定的环境变量,值变为空
+
+[I/O重定向 :在命令中使用< FILE可以将FILE文件作为命令的标准输入]
+[在命令中使用> FILE可以将FILE文件作为命令的标准输出,如果FILE文件已存在则会覆盖写入]
+[在命令中使用>> FILE可以将FILE文件作为命令的标准输出,如果FILE文件已存在则会追加写入]
+[在命令中使用2> FILE可以将FILE文件作为命令的标准错误输出,如果FILE文件已存在则会覆盖写入]
+[在命令中使用2>> FILE可以将FILE文件作为命令的标准错误输出,如果FILE文件已存在则会覆盖写入]
+
+[管道 :执行命令COMMAND1 | COMMAND2 | ... | COMMANDN,可以依次将前一个命令的标准输出作为下一个命令的标准输入]
+[第一个命令的标准输入和最后一个命令的标准输出为默认值]

+ 452 - 0
myshell.go

@@ -0,0 +1,452 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"os/exec"
+	"os/signal"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+	"unicode"
+)
+
+// 内部命令
+var internal = []string{"bg", "cd", "clr", "dir", "echo", "env", "exec", "exit", "fg", "help", "jobs", "pwd", "set", "shift", "test", "time", "umask", "unset"}
+
+// 用户环境变量
+var userenv []string
+
+// 是否从批处理文件执行
+var script bool
+
+// job结构体定义
+type job struct {
+	wait  bool      // 是否有goroutine在等待job结束
+	state string    // job的状态
+	work  string    // 执行的命令
+	cmd   *exec.Cmd // 命令相关信息
+}
+
+var cur job           // 前台执行的命令
+var term chan bool    // 停止信号管道,用于job前后台切换
+var jobs map[int]*job // job映射
+
+var sigs chan os.Signal // 信号管道,用于捕获信号
+
+// 提示符函数
+func prompt() {
+	if !script { // 如果不是从批处理文件执行,则输出提示符
+		dir, err := os.Getwd() // 获取工作目录
+		if err != nil {
+			fmt.Println(err)
+		}
+		fmt.Printf("\033[94m%s\033[0m", dir) // 输出工作目录
+
+		state := os.Getenv("?") // 获取上一条命令的退出状态
+		if state == "0" {       // 根据退出状态显示不同颜色
+			fmt.Print("$ ")
+		} else {
+			fmt.Print("\033[91m$\033[0m ")
+		}
+	}
+}
+
+// 命令分隔函数
+func split(s string) []string {
+	res, str := []string{}, ""    // 返回结果、当前的字符串
+	escape, quote := false, false // 是否转义、是否在双引号内
+
+	for _, c := range s { // 枚举字符串中每个字符
+		if escape { // 如果要转义,则根据当前字符决定转换成什么字符
+			if !quote {
+				str += string(c)
+			} else if c == 'a' {
+				str += "\a"
+			} else if c == 'b' {
+				str += "\b"
+			} else if c == 'f' {
+				str += "\f"
+			} else if c == 'n' {
+				str += "\n"
+			} else if c == 'r' {
+				str += "\r"
+			} else if c == 't' {
+				str += "\t"
+			} else if c == 'v' {
+				str += "\v"
+			} else {
+				str += string(c)
+			}
+			escape = false // 下一个字符不用转义
+		} else if c == '\\' { // 如果是反斜杠
+			escape = true // 则下一个字符需要转义
+		} else if c == '"' { // 如果是双引号
+			quote = !quote // 则切换是否在双引号内的状态
+		} else if unicode.IsSpace(c) && !quote { // 如果是空白字符并且不在双引号内
+			if str != "" {
+				res = append(res, str) // 则要进行分隔
+				str = ""
+			}
+		} else {
+			str += string(c) // 否则,将字符添加到当前字符串末尾
+		}
+	}
+	if str != "" {
+		res = append(res, str)
+	}
+
+	return res
+}
+
+// 命令处理函数
+func process(fields []string, stdin *os.File, stdout *os.File, stderr *os.File) {
+	if len(fields) == 0 {
+		return
+	}
+
+	// 错误处理
+	defer func() {
+		err := recover() // 捕获命令执行过程中发生的错误
+		if err != nil {
+			if err != "" {
+				fmt.Fprintln(stderr, err) // 输出错误信息
+			}
+			os.Setenv("?", "1") // 有错误发生
+			return
+		}
+		os.Setenv("?", "0") // 没有错误发生
+	}()
+
+	// 重定向处理
+	for i := 0; i < len(fields)-1; {
+		switch fields[i] {
+		case "<": // 标准输入
+			file, err := os.Open(fields[i+1])
+			if err != nil {
+				panic(err)
+			}
+			defer file.Close()
+			stdin = file
+			fields = append(fields[:i], fields[i+2:]...) // 从命令中删除
+		case ">": // 覆盖标准输出
+			file, err := os.OpenFile(fields[i+1], os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+			if err != nil {
+				panic(err)
+			}
+			defer file.Close()
+			stdout = file
+			fields = append(fields[:i], fields[i+2:]...)
+		case ">>": // 追加标准输出
+			file, err := os.OpenFile(fields[i+1], os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
+			if err != nil {
+				panic(err)
+			}
+			defer file.Close()
+			stdout = file
+			fields = append(fields[:i], fields[i+2:]...)
+		case "2>": // 覆盖标准错误输出
+			file, err := os.OpenFile(fields[i+1], os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+			if err != nil {
+				panic(err)
+			}
+			defer file.Close()
+			stderr = file
+			fields = append(fields[:i], fields[i+2:]...)
+		case "2>>": // 追加标准错误输出
+			file, err := os.OpenFile(fields[i+1], os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
+			if err != nil {
+				panic(err)
+			}
+			defer file.Close()
+			stderr = file
+			fields = append(fields[:i], fields[i+2:]...)
+		default:
+			i++
+		}
+	}
+
+	if len(fields) == 0 {
+		fields = []string{"more"} // 如果没有命令,则调用more命令显示标准输入
+	}
+
+	// 后台执行处理
+	if fields[len(fields)-1] == "&" {
+		in := false // 判断是否属于内部命令
+		for _, cmd := range internal {
+			if fields[0] == cmd {
+				in = true
+				break
+			}
+		}
+
+		i := 1 // 获取job的id
+		for ; ; i++ {
+			_, ok := jobs[i]
+			if !ok {
+				break
+			}
+		}
+		jobs[i] = &job{false, "running", strings.Join(fields[:len(fields)-1], " "), nil} // 创建job
+
+		if in { // 如果是内部命令,则调用shell执行
+			jobs[i].cmd = exec.Command(os.Getenv("SHELL"), "-")
+			jobs[i].cmd.Stdin = strings.NewReader(jobs[i].work)
+		} else { // 否则,直接调用
+			jobs[i].cmd = exec.Command(fields[0], fields[1:len(fields)-1]...)
+			jobs[i].cmd.Stdin = stdin
+		}
+		jobs[i].cmd.Stdout = stdout
+		jobs[i].cmd.Stderr = stderr
+		jobs[i].cmd.Env = append(userenv, "PARENT="+os.Getenv("SHELL")) // 添加parent环境变量
+
+		err := jobs[i].cmd.Start() // 开始执行
+		if err != nil {
+			panic(err)
+		}
+
+		go func() { // 创建goroutine
+			err := jobs[i].cmd.Wait() // 等待job结束
+			if err != nil {
+				fmt.Println(err)
+			}
+			defer func() {
+				recover()
+			}()
+			term <- true // 向停止信号管道发送信号,表示程序退出
+		}()
+		jobs[i].wait = true // 有goroutine在等待job结束
+
+		fmt.Printf("[%d] %d\n", i, jobs[i].cmd.Process.Pid)
+		return
+	}
+
+	// 命令解析
+	switch fields[0] {
+	case "bg": // 后台执行job
+		cmd_bg(fields, stdin, stdout, stderr)
+
+	case "cd": // 修改工作目录
+		cmd_cd(fields, stdin, stdout, stderr)
+
+	case "clr": // 清屏
+		if len(fields) > 1 {
+			panic("clr: too many arguments")
+		}
+		fmt.Fprint(stdout, "\033[H\033[2J") // 通过转义字符实现清屏
+
+	case "dir": // 显示目录内容
+		cmd_dir(fields, stdin, stdout, stderr)
+
+	case "echo": // 显示内容并换行
+		cmd_echo(fields, stdin, stdout, stderr)
+
+	case "env": // 显示用户环境变量
+		cmd_env(fields, stdin, stdout, stderr)
+
+	case "exec": // 执行命令
+		process(fields[1:], stdin, stdout, stderr)
+
+	case "exit": // 退出shell
+		cmd_exit(fields, stdin, stdout, stderr)
+
+	case "fg": // 前台执行job
+		cmd_fg(fields, stdin, stdout, stderr)
+
+	case "jobs": // 显示job列表
+		cmd_jobs(fields, stdin, stdout, stderr)
+
+	case "pwd": // 显示工作目录
+		dir, err := os.Getwd() // 获取工作目录
+		if err != nil {
+			panic(err)
+		}
+		fmt.Fprintln(stdout, dir)
+
+	case "set": // 显示所有环境变量或设置命令行参数
+		cmd_set(fields, stdin, stdout, stderr)
+
+	case "shift": // 左移参数
+		cmd_shift(fields, stdin, stdout, stderr)
+
+	case "test": // 测试条件
+		cmd_test(fields, stdin, stdout, stderr)
+
+	case "time": // 显示时间
+		t := time.Now()
+		fmt.Fprintln(stdout, t)
+
+	case "umask": // 显示或设置umask
+		cmd_umask(fields, stdin, stdout, stderr)
+
+	case "unset": // 重设变量
+		cmd_unset(fields, stdin, stdout, stderr)
+
+	default: // 外部命令
+		if fields[0] == "help" { // 显示用户手册
+			fields[0] = "more" // 调用more命令过滤
+			cur = job{false, "running", strings.Join(fields, " "), exec.Command(fields[0], fields[1:]...)}
+			cur.cmd.Stdin = manual()
+		} else {
+			cur = job{false, "running", strings.Join(fields, " "), exec.Command(fields[0], fields[1:]...)} // 前台执行
+			cur.cmd.Stdin = stdin
+		}
+		cur.cmd.Stdout = stdout
+		cur.cmd.Stderr = stderr
+		cur.cmd.Env = append(userenv, "PARENT="+os.Getenv("SHELL")) // 添加parent环境变量
+
+		err := cur.cmd.Start() // 开始执行
+		if err != nil {
+			panic(err)
+		}
+
+		term = make(chan bool) // 初始化停止信号管道
+
+		go func() { // 创建goroutine
+			err := cur.cmd.Wait() // 等待job结束
+			if err != nil {
+				fmt.Println(err)
+			}
+			defer func() {
+				recover()
+			}()
+			term <- true // 向停止信号管道发送信号,表示程序退出
+		}()
+		cur.wait = true // 有goroutine在等待job结束
+
+		<-term        // 等待接收信号,实现前台执行
+		close(term)   // 关闭停止管道
+		cur.cmd = nil // 前台没有在执行命令
+	}
+}
+
+// 管道处理函数
+func pipe(fields []string) {
+	if len(fields) == 0 {
+		return
+	}
+
+	sub := []string{} // 子命令
+
+	stdin := os.Stdin                // 子命令标准输入
+	_stdin, stdout, err := os.Pipe() // 在当前子命令标准输出和下一个子命令标准输入之间建立管道
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	stderr := os.Stderr // 子命令标准错误输出
+
+	for _, field := range fields {
+		if field == "|" { // 如果是管道分隔符
+			if len(sub) != 0 {
+				process(sub, stdin, stdout, stderr) // 则处理子命令
+				stdout.Close()                      // 关闭当前子命令标准输出,表示处理完成
+				stdin = _stdin                      // 设置下一个子命令标准输入
+				_stdin, stdout, err = os.Pipe()     // 继续建立管道
+				if err != nil {
+					fmt.Println(err)
+					return
+				}
+				sub = []string{}
+			}
+		} else {
+			sub = append(sub, field)
+		}
+	}
+	stdout = os.Stdout // 最后一个子命令标准输出为默认值
+	if len(sub) != 0 {
+		process(sub, stdin, stdout, stderr)
+	}
+}
+
+// 主函数
+func main() {
+	userenv = os.Environ() // 设置用户环境变量
+
+	// 初始化shell环境变量
+	os.Setenv("!", "0")                            // 最近一次后台进程的pid
+	os.Setenv("#", strconv.Itoa(len(os.Args)-1))   // 命令行参数个数
+	os.Setenv("$", strconv.Itoa(os.Getpid()))      // 当前进程的pid
+	os.Setenv("?", "0")                            // 上一条命令的退出状态
+	os.Setenv("*", strings.Join(os.Args[1:], " ")) // 所有命令行参数的值
+	for i, str := range os.Args {
+		os.Setenv(strconv.Itoa(i), str) // 命令行参数
+	}
+	path, err := filepath.Abs(os.Args[0]) // 执行shell的绝对路径
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+	os.Setenv("SHELL", path) // 添加shell环境变量
+
+	script = len(os.Args) > 1 // 是否指定批处理文件
+
+	if script && os.Args[1][0] != '-' {
+		file, err := os.Open(os.Args[1]) // 打开文件
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+		os.Stdin = file // 将标准输入重定向到文件
+	}
+
+	sigs = make(chan os.Signal, 1) // 初始化信号管道
+	// 捕获SIGINT、SIGQUIT、SIGTSTP、SIGCHLD信号
+	signal.Notify(sigs, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTSTP, syscall.SIGCHLD)
+
+	// 信号处理
+	go func() {
+		for sig := range sigs {
+			switch sig {
+			case syscall.SIGINT: // Ctrl+C触发SIGINT信号
+				fmt.Println() // 换行
+				prompt()      // 输出提示符
+
+			case syscall.SIGTSTP: // Ctrl+Z触发SIGTSTP信号
+				if cur.cmd != nil { // 如果有前台执行的命令
+					i := 1 // 获取job的id
+					for ; ; i++ {
+						_, ok := jobs[i]
+						if !ok {
+							break
+						}
+					}
+					jobs[i] = &job{cur.wait, "suspended", cur.work, cur.cmd} // 创建job
+
+					term <- false // 向停止信号管道发送信号,表示进程暂停
+					fmt.Printf("[%d]    %d suspended\t%s\n", i, jobs[i].cmd.Process.Pid, jobs[i].work)
+				}
+
+			case syscall.SIGCHLD: // 子进程结束触发SIGCHLD信号
+				for i, job := range jobs { // 遍历所有job
+					ps := job.cmd.ProcessState
+					if ps != nil && ps.Exited() { // 如果job已经退出
+						if ps.Success() { // 根据退出状态显示不同信息
+							fmt.Printf("[%d]    %d done\t%s\n", i, job.cmd.Process.Pid, job.work)
+						} else {
+							fmt.Printf("[%d]    %d exit %d\t%s\n", i, job.cmd.Process.Pid, ps.ExitCode(), job.work)
+						}
+						delete(jobs, i) // 删除job
+					}
+				}
+			}
+		}
+	}()
+
+	scanner := bufio.NewScanner(os.Stdin) // 按行读入
+
+	jobs = make(map[int]*job) // 初始化job映射
+
+	prompt()             // 每次读入前输出提示符
+	for scanner.Scan() { // 读入一整行
+		cmds := strings.Split(scanner.Text(), ";") // 按分号分隔
+		for _, cmd := range cmds {                 // 依次处理
+			pipe(split(cmd)) // 按空白字符分隔后进行管道处理
+		}
+		prompt()
+	}
+	fmt.Println()
+}