Lab6-Challenge实验报告
一、总述
上图是lab6中 实现的shell,在此基础上,进行更改,完成了challenge任务,修改后的shell如下图
其中,蓝色部分对应Easy部分要求的实现后台运行、一行多命令、简单引号支持以及新增外部命令touch/mkdir/tree,黄色部分为Normal部分中支持历史指令回显功能,紫色部分为Challenge部分简单支持环境变量部分,通过在内核中维护环境变量,使用系统调用访问的方式实现。具体实现细节见第二部分。
二、具体实现
2.1 Easy 部分
2.1.1 实现后台运行
用一个变量记录是否需要后台运行,如果需要后台运行,则此进程不等待spawn出的进程直接退出,从而可以获取新的输入
//runcmd
int run_back = 0;/*是否需要后台运行*/
int back_id/*后台运行的进程号*/
/*在处理gettoken的循环中新增一个case &,记录是否需要后台运行*/
for(;;){
...
case '&':
run_back = 1;
break;
...
}
// runit:
if(!run_back){//如果不需要run back, 等待spawn出的进程r结束
if (debug_) writef("[%08x] WAIT %s %08x\n", env->env_id, argv[0], r);
wait(r);
}else{//需要run back
back_id = fork();//fork 出一个进程等着r
if(back_id == 0){//输出任务开始和结束的信息
writef(LIGHT_GREEN(\n[%08x] running\t), r);
for(i=0; i<argc; i++){
writef(LIGHT_GREEN(%s)" ", argv[i]);
}
wait(r);
writef("\n[%08x] done\t", r);
for(i = 0; i<argc;i++){
writef("%s ", argv[i]);
}
writef("\n");
exit();
}
//此进程直接退出,从而使等待此进程的主进程umain可以继续获取输入
}
由于前台进程在readline中获取输入时,最终调用的函数是sys_cgetc,而这个函数在等待输入时是忙等,一直占用CPU,无法被其他进程打断, 因此需要对此进行修改,不然前台进程将一直占用CPU,spawn出的进程将无法后台执行,修改后即可实现后台进程运行时继续输入新指令
//lib/getc.S
.set noreorder
LEAF(sys_cgetc)
1: lb t0, 0x90000000
/*这里在不读入字符时死循环,导致忙等*/
// beqz t0, 1b
// nop
move v0,t0
jr ra
nop
END(sys_cgetc)
2.1.2 实现一行多命令
在处理gettoken的循环中新增一个;,仿照管道|的情况,fork出一个新的进程,子进程继续解析;右侧的命令,左侧直接运行 goto runit;
case ';':
if(isright = fork() == 0 ){
goto again;
}else{
goto runit;
}
break;
}
2.1.3 实现引号支持
添加两个变量ignore1 & ignore2分别代表检测到了一个' or "的情况
在_gettoken中,添加对”/'的判断,在读到这两种符号,也就是ignore1 != 0 || ignore2 != 0时不识别后续的标识符,当取消ignore之后,在合适的位置返回一个word
//in _gettoken(char *s, char **p1, char **p2)
int ignore1 = 0;
int ignore2 = 0;
if(*s == '\''){
ignore2 = 1-ignore2;
strcpy(s, s+1);
}
if(*s == '\"'){
ignore2 = 1-ignore2;
strcpy(s, s+1);
}
//... some code else ...
while(*s && (!strchr(WHITESPACE SYMBOLS, *s) || !(ignore1 == 0 && ignore2 == 0) )) {
if(*s == '\''){
ignore1 = 1-ignore1;
strcpy(s, s+1);
}
else if(*s == '\"'){
ignore2 = 1-ignore2;
strcpy(s, s+1);
}else{
s++;
}
}
2.1.4 实现mkdir & touch
2.1.4.1实现user_create
为了更加方便快捷的创建文件和目录,观察到文件系统fs/fs.c中有函数file_create, 但没有供用户使用的用户接口,因此在fs/serv.c中实现serve_create,以及在user/fsipc.c中实现fsipc_create,从而添加文件系统调用接口,在user/file.c中实现user_create,使得用户可以通过调用这个函数新建文件并指定文件类型。
void
serve_create(u_int envid, struct Fsreq_create *rq){
u_char path[MAXPATHLEN];
int isdir = rq -> req_isdir;
struct File *f;
int fileid;
int r;
user_bcopy(rq->req_path, path, MAXPATHLEN);
path[MAXPATHLEN - 1] = 0;
if((r = file_create((char *)path, &f)) < 0 ){
ipc_send(envid, r, 0, 0);
return;
}
f->f_type = isdir;
ipc_send(envid, 0, 0, 0);
}
int fsipc_create(char* path, int isdir){
u_int perm;
struct Fsreq_create *req;
req = (struct Fsreq_create *)fsipcbuf;
if (strlen(path) >= MAXPATHLEN ){
return -E_BAD_PATH;
}
strcpy((char *)req->req_path, path);
req->req_isdir = isdir;
return fsipc(FSREQ_CREATE, req, 0, &perm);
}
int user_create(char* path, int isdir){
int r = fsipc_create(path, isdir);
return r;
}
2.1.4.2 实现 touch & mkdir
只需调用上述用户函数,将isdir分别设置成0和1即可
//user/touch.c
r = user_create(argv[1], 0);
if(r < 0){
writef("\ncannot create file %s: error: %d", argv[1], r );
}
//user/mkdir.c
r = user_create(argv[1], 1);
if(r < 0){
writef("cannot create directory \'%s\': %d ",argv[1], r );
}
2.1.5 实现tree
tree指令需要打开指定的路径并显示其中的文件,遇到目录时,只需要递归的调用即可。当一个文件是这个目录中的最后一个时,后续的文件树不显示|,因此需要记录当前文件的各级父文件是否是最后一个文件,从而正确的打印文件树。
int tree(char *path,int deep, int *isEnd){
int fd;
struct File f, fnext;
int r,n;
int nowEnd = 0;
char next_path[MAXPATHLEN];
if((fd = open(path, O_RDONLY)) < 0 ){
//...
}
//...
if((r = readn( fd, &fnext, sizeof f)) != sizeof f || fnext.f_name[0] == '\0'){
return;
}
while(nowEnd != 1){
f = fnext;
if((r = readn( fd, &fnext, sizeof f)) != sizeof f || fnext.f_name[0] == '\0'){
nowEnd = 1;//当前时最后一个
}
if(f.f_name[0] != '\0'){
//...当前文件不为空,输出文件名
}
if(f.f_type == FTYPE_DIR){//当前文件是目录
isEnd[deep] = nowEnd;
strcpy(next_path, path);
strcpy(next_path + strlen(next_path), "/");
strcpy(next_path + strlen(next_path), f.f_name);
tree(next_path, deep + 1, isEnd);//递归调用
}
}
close(fd);
}
/*按照当前深度和各级父目录情况输出文件树枝*/
void print_bar(int deep, int* isEnd){
int i;
int tot = deep * 3;
for(i=0 ;i<tot; i++){
if(i % 3 == 0 && isEnd[i/3] != 1){
writef("|");
}else{
writef(" ");
}
}
}
2.2 Normal部分 – 历史命令
Normal部分是需要支持历史指令回显,只需要在每次输入完指令后,将字符串以O_APPEND的方式写入磁盘中的.hestory文件(以O_CREAT模式打开文件,从而在第一次调用时创建文件),并在readline遇到↑ ↓控制符号的时候读.history文件,将特定的历史记录回显到屏幕即可。history指令则是直接输出.history文件的全部内容即可。
2.2.1 支持以 O_CREAT 和 O_APPEND 模式打开文件
//lib.h
#define O_APPEND 0x0004//新增打开模式
//user/file.c/open
/*O_APPEND*/
if((mode & O_APPEND) != 0){
ffd->f_fd.fd_offset = ffd->f_file.f_size;
}
//fs/serv.c/serve_open
/*O_CREATE*/
if ((r = file_open((char *)path, &f)) < -1) {
if(r == -E_NOT_FOUND && ((rq->req_omode & O_CREAT) != 0)) {
if((r = file_create((char*)path, &f)) < 0 ){
ipc_send(envid, r, 0, 0);
return;
}
f->f_type = FTYPE_REG;
2.2.2 read_history函数和write_history函数
int read_history_cached(char history_cmd[128][128]){
int index = ((history_cnt - 128) > 0) ? history_cnt-128 : 0;
int i;
char buf[128];
int fd = open("/.history", O_RDONLY);
if(fd < 0){
return 0;
}
for(i = 0; i< index; i++){
read_line(fd, buf, 128);//从文件中读入一行,限制最大读入数,返回值为实际读入字符数
}
for(i=index; i < history_cnt; i++){
read_line(fd, buf, 128);
strcpy(history_cmd[i-index], buf);
}
close(fd);
return history_cnt - index;
}
void write_history(char* buf, int n ){
static int run_time = 0;
int fd, r;
if(run_time == 0){
user_create("/.history", 0);
}
fd = open("/.history", O_WRONLY | O_APPEND );
r = write(fd, buf, n);
if(r < n){
writef("\nerror in write_history\n");
}
r = write(fd, "\n", 1);
if(r < 1){
writef("\nerror in write_history\n ");
}
close(fd);
history_cnt += 1;
run_time++;
}
值得注意的是,write_history需要在readline之后立即写入,原样写入输入的命令。
为了防止多次打开关闭文件造成性能影响,read_history一次读取最近的多行历史,通过在readline函数中判断上下箭头控制号
↑:\033[A 若至屏幕顶端则无效
↓:\033[B 若至屏幕底端则无效
从而回显特定的history
if(i >= 2 && buf[i - 2] == 27 && buf[i-1] == 91 && buf[i] == 65){//上箭头\033[A
writef("%c%c%c", 27, 91, 66);
for(i -= 2; i; --i) writef("\b \b");//清空显示
if(cnt > 0){
strcpy(buf, historys[--cnt]);//显示历史
}else{
strcpy(buf, historys[cnt]);
}
writef("%s", buf);
i = strlen(buf) -1;
}
if(i >= 2 && buf[i - 2] == 27 && buf[i - 1] == 91 && buf[i] == 66){//下箭头\033[B
// writef("%c%c%c", 27, 91, 65);屏幕底端无效
if(cnt < sum - 1){
for(i -= 2; i; --i) writef("\b \b");//清空显示
strcpy(buf, historys[++cnt]);//显示历史
writef("%s", buf);
i = strlen(buf) - 1;
}//....
2.2.3 history指令
将.history文件中的内容都输出即可
int fd = open("/.history",O_RDONLY);
char buf[128];
int r;
if(fd < 0){
writef("\n./history not found\n");
}
int cnt = 1;
while((r = read_line(fd, buf, 128)) != 0 ){
writef(MEG(%d), cnt);
writef("\t%s\n", buf);
cnt++;
}
2.3 Challenge 部分 – 环境变量
2.3.1 环境变量的维护
为了实现不同进程之间声明的局部变量不可见,环境变量可见,选择在内核当中记录变量名以及相应信息。需要记录当前变量是否为环境变量,声明这个变量的进程号,变量值,是否只读。并通过系统调用的方式为用户提供服务。
采用哈希线性探测查找和维护变量表
主要代码为lib/syscall_all.c中的sys_env_var
#define ENVVAR_CLEAR (1 << 7)
#define GET (1 << 6)
#define LIST (1 << 5)
#define UNSET (1 << 4)
#define CREATE (1 << 3)//if set, return value, else set name
#define SET (1 << 2) // recover value, or glob readonly
#define GLOB (1 << 1)
#define RDONLY (1 << 0)
int sys_env_var(int sysno, char* name, char* value, u_int env_id, u_int option){
static int cnt = 0;
static char name_list[NHASH][32] = {0};
static u_int envid_list[NHASH] = {0};
static char value_list[NHASH][128];
static int readonly[NHASH] = {0} ;
static int glob[NHASH] = {0};
int i;
if(option & ENVVAR_CLEAR){
for(i = 0; i < NHASH; i++ ){
if(glob[i] != 1 && envid_list[i] == env_id ){
name_list[i][0] = 0;
value_list[i][0] = 0;
envid_list[i] = 0;
glob[i] = 0;
readonly[i] = 0;
}
}
return 0;
}
/*列出所有env_id进程可见的变量*/
if(option & LIST){
for(i=0; i < NHASH ; i++){
if(name_list[i][0] != 0){
if(glob[i] == 1 || envid_list[i] == env_id ){
printf("%s=%s\n", name_list[i], value_list[i]);
}
}
}
return 0;
}
/*哈希线性探测查找*/
u_int index = hash(name);
while(name_list[index][0]){
if(strcmp(name_list[index], name) == 0 && (env_id == envid_list[index] || glob[index] == 1 )){
if ((option & CREATE)) {
printf("Already declaired: %s=%s", name_list[index],value_list[index]);
return -16;
}
break;
}else{
index++;
if(index == NHASH ) index =0;
}
}
/*设置变量*/
if((option & SET)){
strcpy(name_list[index], name);
if(!readonly[index]){
strcpy(value_list[index], value);/*记录值*/
}else{
printf("\nfailed,\"%s\" is readonly\n", name);
}
if((option & GLOB) && glob[index] == 0 ){/*设置全局*/
glob[index] = 1;
envid_list[index] = 0;
}else if(glob[index] == 0 ) {/*设置非全局*/
envid_list[index] = env_id;
}
readonly[index] |= (option & RDONLY);/*设置只读*/
}else if(option & CREATE){/*创建变量*/
strcpy(name_list[index], name);
value_list[index][0] = 0;
if((option & GLOB) && glob[index] == 0 ){
glob[index] = 1;
envid_list[index] = 0;
}else if(glob[index] == 0 ) {
envid_list[index] = env_id;
}
readonly[index] |= (option & RDONLY);
}else if(option == GET){/*查找变量*/
if(strcmp(name_list[index], name) != 0){
value[0] = 0;
return -E_VAR_NOT_FOUND;
}
strcpy(value, value_list[index]);
}else if(option & UNSET){/*销毁变量*/
if(strcmp(name_list[index], name) != 0){
printf("\nfailed, \"%s\" is not found", name);
return -E_VAR_NOT_FOUND;
}
if(readonly[index] == 1){
printf("\nfailed, \"%s\" is readonly", name);
return -E_VAR_READONLY;
}
name_list[index][0] = 0;
value_list[index][0] = 0;
glob[index] = 0;
readonly[index] = 0;
envid_list[index] = 0;
}
return 0;
}
2.3.2 declare & unset命令
使用内建命令,在使用命令的时候不需要spawn单独创建一个进程装载命令的可执行文件。内部命令和shell一起常驻内存。
//sh.c
void declare(int argc, char ** argv, u_int env_id){
u_int option = 0;
int i;
ARGBEGIN/*处理选项*/
{
default:
fwritef(1, RED(\nusage: declare [-xr] [NAME[=VALUE]]\n));
return;
case 'r'://-r readonly
option |= ENVVAR_RDONLY;
break;
case 'x'://-x set environmentaly
option |=ENVVAR_GLOB;
break;
}
ARGEND
char name[32], value[128];
char *p;
int r;
if(argc > 1){
fwritef(1, RED(\ntoo many argvs\n));
return;
}
if(argc == 1){
if((p = strchr(argv[0], '=')) > 0){
option |= ENVVAR_SET;
strcpy(name, argv[0]);
name[p-argv[0]] = 0;
p++;
strcpy(value, p);
}else{
option |= ENVVAR_CREATE;
strcpy(name, argv[0]);
}
syscall_env_var(name, value, env_id, option);//
}else{
option |= ENVVAR_LIST;
syscall_env_var( 0, 0, env_id, option);
}
}
void unset(int argc, char **argv, u_int env_id){
u_int option = 0;
if(argc > 2){
fwritef(1, RED(\ntoo many argvs\n));
return;
}
else if(argc < 2){
fwritef(1, RED(\ntoo few argvs\n));
return;
}
else{
option |= ENVVAR_UNSET;
syscall_env_var(argv[1], 0, env_id, option);
}
}
2.3.3 支持$x变量替换 & 销毁进程同时销毁进程的局部变量
支持如 echo $a变量替换,需要在将命令送入runcmd之前,对所有不在' '内部的变量进行字符串替换。
#define BREAK_SIG " \'\t\r\n$"
void replace_envvar(char *buf, u_int env_id){
int ignore = 0;
char* p = buf;
char name[32] ={0};
int namelen = 0;
char value[128] = {0};
char temp[1024] = {0};
int i;
while(*p){
if(*p == '\''){
ignore = 1 - ignore;
p++;
}else if(*p == '$' && !ignore){
for(namelen = 1; *(p + namelen) != 0 && !(strchr(BREAK_SIG, *(p+namelen))); namelen++){
name[namelen-1] = *(p + namelen);
}
getvar(name, env_id, value);//通过系统调用获取变量值
//...省略进行替换的字符串操作
}else{
p++;
}
}
}
需要通过系统调用获取环境变量的值
void getvar(char *name, u_int env_id, char* value){
u_int option = 0;
option |= ENVVAR_GET;
syscall_env_var(name, value, env_id, option);
}
特别的,当一个shell被销毁时,它声明的局部变量也同时失效
void clear_envvar(u_int env_id, u_int option){
syscall_env_var(0, 0, env_id, ENVVAR_CLEAR);
}
2.4 其他
2.4.1 支持退格
if(buf[i] == '\b' || buf[i] == 127){
if(i > 0){
buf[i] = 0;
clear_con(buf);
buf[i-1]=0;
writef("%s", buf);
i -= 2;
}else{
buf[i] = 0;
i = -1;
}
}
2.4.2 支持自动添加.b
strcpy(prog_path, prog);
int len = strlen(prog_path);
if(prog_path[len-2] != '.' || prog_path[len-1] != 'b'){
strcpy(prog_path+strlen(prog_path), ".b");
}
if((r=open(prog_path, O_RDONLY))<0){
2.4.3 支持彩色输出
使用ANSI转义序列
#define LIGHT_BLUE(str) "\033[0m\033[1;34m" # str "\033[m"
#define BLUE(str) "\033[0m\033[34m" # str "\033[m"
#define DARK_BLUE(str) "\033[0m\033[2;34m" # str "\033[m"
#define RED(str) "\033[0m\033[31m" # str "\033[m"
#define LIGHT_RED(str) "\033[0m\033[1;31m" # str "\033[m"
#define DARK_RED(str) "\033[0m\033[2;31m" # str "\033[m"
#define GREEN(str) "\033[0m\033[32m" # str "\033[m"
#define LIGHT_GREEN(str) "\033[0m\033[1;32m" # str "\033[m"
#define DARK_GREEN(str) "\033[0m\033[2;32m" # str "\033[m"
#define YELLOW(str) "\033[0m\033[33m" # str "\033[m"
#define LIGHT_YELLOW(str) "\033[0m\033[1;33m" # str "\033[m"
#define DARK_YELLOW(str) "\033[0m\033[2;33m" # str "\033[m"
#define MEG(str) "\033[0m\033[35m" # str "\033[m"
#define LIGHT_MEG(str) "\033[0m\033[1;35m" # str "\033[m"
#define DARK_MEG(str) "\033[0m\033[2;35m" # str "\033[m"
#define CIY(str) "\033[0m\033[36m" # str "\033[m"
#define LIGHT_CIY(str) "\033[0m\033[1;36m" # str "\033[m"
#define DARK_CIY(str) "\033[0m\033[2;36m" # str "\033[m"
三、测试&运行效果
3.1 Easy部分
3.1.1后台运行
测试程序,利用大循环实现长时间后台运行方便观察
#include "lib.h"
void umain(int argc, char **argv){
int N = (argv[1][0] - '0') * 10000000 , i;
for(i = 0; i < N; i++ ){
if(i == N/10){
writef("\n-%%10-");
}
if(i == N/10 * 2){
writef("\n--%%20--");
}
if(i == N / 10 *3){
writef("\n---%%30---");
}
if(i == N / 10 *4){
writef("\n----%%40----");
}
if(i == N / 10 *5){
writef("\n-----%%50-----");
}
if(i == N / 10 *6){
writef("\n------%%60------");
}
if(i == N / 10 *7){
writef("\n--------%%70-------");
}
if(i == N / 10 *8){
writef("\n---------%%80--------");
}
if(i == N / 10 *9){
writef("\n-----------%%90---------");
}
}
writef("\nFINISHED");
可见在后台程序运行时,仍然可以输入指令tree并显示文件树。
3.1.2多命令
3.1.3 tree/mkdir/touch
通过mkdir/touch新建文件,通过tree显示结果。测试命令见附录。
3.2 Normal部分
通过history命令显示历史命令
3.3 Challenge部分
可见,变量定义正常,只读变量无法被修改或取消,子进程只能看到环境变量,无法看到局部变量,echo支持变量带入值,支持' '中原样输出
四、实验困难及解决
4.1 Easy部分中实现后台运行
实现后台运行的难点主要在找出前台进程等待的位置,一开始,我只注意到了runcmd中runit里等待被spawn的进程的问题,在进行测试时,我发现,虽然运行测试程序后,可以立即显示一个新的$提示我继续输入,但我必须进行一些输入后,后台进程才有运行反应,如果我一直不输入,后台进程则一直不会运行。
通过仔细阅读challenge题目提示,我理解了提示中的忙等是指前台进程一直在内核态等待输入死循环,无法被时钟打断,从而一直占有CPU导致其他进程无法被调度,失去执行机会。最终通过找到sys_get_char中的死循环并删除解决。
4.2 Normal部分中关于历史记录条数的维护
在维护历史记录的过程中,我的初次尝试是在write_history中维护一个static变量来记录历史记录条目数,并且在run_cmd中记录历史。
void write_history(char* buf, int n ){
static int history_cnt = 0;
int index = ((history_cnt - 128) > 0) ? history_cnt-128 : 0;
void
runcmd(char *s, u_int env_id)
{//...some defination
rightpipe = 0;
write_history(s,strlen(s));
gettoken(s, 0);
again:
我发现这样的写法下,我的history_cnt变量在进入write_history函数后能正常+1,但退出之后,执行下一条命令时,又变回0,无法读取历史命令。
经检查发现,由于runcmd时,主进程已经fork了一个进程并且由子进程runcmd,并且history_cnt所在的页面不是共享页面,因此history_cnt在子进程中的改变无法传递给父进程,因此父进程在下次read时依然认为history_cnt为0
改进后,调整了记录history的位置,改在了fork之前,readline之后,问题迎刃而解。
readline(buf, sizeof buf);
write_history(buf, strlen(buf));
//...some code not important
if ((r = fork()) < 0)
user_panic("fork: %e", r);
4.3 Challenge部分中逻辑复杂问题
这部分中由于对环境变量的操作非常多,并且还有选项的组合问题,局部变量和环境变量的问题,使得一开始做起来问题非常复杂。
首先是如何判断查找到的变量是否对于这个进程可见,也就是如何判断命中的问题。思考过后发现这个问题与TLB命中十分类似,TLB中有g(globle)位和asid来判断表项是否属于这个进程,那么这里也可以使用一个glob变量和env_id变量判断。当glob为0也就是局部变量时,进行name和env_id的双关键字比较,当glob为1时,只进行name比较
第二个问题是,-x和-r选项使得declare变得非常灵活,-x-r和declare的组合形式很多,而系统调用的参数传递量最大为5,非常有限。经思考发现这个选项模式很类似于文件打开的O_MODE,选项之间是逻辑或的关系,因此我仿照O_MODE,采用了二进制掩码的方式表示Option,这样只需要传递一个参数即可表达清楚操作内容。
/*ENV VAR OP*/
#define ENVVAR_CLEAR (1 << 7)
#define ENVVAR_GET (1 << 6)
#define ENVVAR_LIST (1 << 5)
#define ENVVAR_UNSET (1 << 4)
#define ENVVAR_CREATE (1 << 3)//if set, return value, else set name
#define ENVVAR_SET (1 << 2) // recover value, or glob readonly
#define ENVVAR_GLOB (1 << 1)
#define ENVVAR_RDONLY (1 << 0)
附录
mkdir courses
mkdir courses/gradeTwo
mkdir courses/gradeTwoSpring
mkdir courses/blog
touch courses/chapter6.zip
touch courses/data_structure.zip
touch courses/math.c
touch courses/physics.c
mkdir courses/gradeTwo/computer_orgnisim
touch courses/gradeTwo/computer_orgnisim/P1.v
touch courses/gradeTwo/computer_orgnisim/P2.v
touch courses/gradeTwo/computer_orgnisim/P3.v
mkdir courses/gradeTwo/computer_orgnisim/reports
touch courses/gradeTwo/computer_orgnisim/reports/reports1.pdf
touch courses/gradeTwo/computer_orgnisim/reports/reports2.pdf
touch courses/gradeTwo/computer_orgnisim/reports/reports3.pdf
mkdir courses/gradeTwo/physics
touch courses/gradeTwo/physics/chapter6.ppt
mkdir courses/gradeTwoSpring/OO
mkdir courses/gradeTwoSpring/OS
mkdir courses/gradeTwoSpring/OO/Homework1
mkdir courses/gradeTwoSpring/OO/Homework2
mkdir courses/gradeTwoSpring/OO/Homework3
mkdir courses/gradeTwoSpring/OO/Homework4
mkdir courses/gradeTwoSpring/OO/Homework5
mkdir courses/gradeTwoSpring/OO/Homework6
mkdir courses/gradeTwoSpring/OO/Homework7
mkdir courses/gradeTwoSpring/OO/Homework8
mkdir courses/gradeTwoSpring/OO/Homework9
mkdir courses/gradeTwoSpring/OO/Homework1/src
touch courses/gradeTwoSpring/OO/Homework1/src/hw1.jar
mkdir courses/gradeTwoSpring/OO/Homework2/src
touch courses/gradeTwoSpring/OO/Homework2/src/hw2.jar
mkdir courses/gradeTwoSpring/OO/Homework3/src
touch courses/gradeTwoSpring/OO/Homework3/src/hw3.jar
mkdir courses/gradeTwoSpring/OO/Homework4/src
touch courses/gradeTwoSpring/OO/Homework4/src/hw4.jar
mkdir courses/gradeTwoSpring/OO/Homework5/src
touch courses/gradeTwoSpring/OO/Homework5/src/hw5.jar
mkdir courses/gradeTwoSpring/OS/OSLab
mkdir courses/gradeTwoSpring/OS/OSTheory
mkdir courses/gradeTwoSpring/OS/OSLab/reports
touch courses/gradeTwoSpring/OS/OSLab/reports/reports1.pdf
touch courses/gradeTwoSpring/OS/OSLab/reports/reports2.pdf
touch courses/gradeTwoSpring/OS/OSLab/reports/reports3.pdf
touch courses/gradeTwoSpring/OS/OSLab/reports/reports4.pdf
touch courses/gradeTwoSpring/OS/OSLab/reports/reports5.pdf
touch courses/gradeTwoSpring/OS/OSLab/reports/reports6.pdf
touch courses/gradeTwoSpring/OS/OSLab/reports/reports9.pdf
touch courses/gradeTwoSpring/OS/OSLab/reports/reports7.pdf
touch courses/gradeTwoSpring/OS/OSLab/reports/reports8.pdf
touch courses/gradeTwoSpring/OS/OSTheory/PPT1.pptx
touch courses/gradeTwoSpring/OS/OSTheory/PPT2.pptx
touch courses/gradeTwoSpring/OS/reports.zip
-----------------------------------showing--------------------------------------
tree
tree /
tree courses
tree courses/gradeTwoSpring/OO
history
/*show choose history by up & down*/