『linux』c 语言实现一个简易的 shell

Table of Content

为了让用户可以控制系统,Linux 系统一般会运行一个 shell 程序。通常来说,shell 程序不会是系统启动后运行的第一个进程(也就是 init 进程), 下面通过c语言来实现一个简单的shell. 首先实现大致框架, 然后逐步增强,添加功能.
它支持一些内部命令, 如 pwd, ls, cd, cat, env, export, unset 以及外部命令
支持一些特色

  • features:

    • \t support redundant blank(\t, spaces)
    • " ' support quote
    • \ multi-line input
    • | pipe
    • < > >> redirect
    • ; multi-cmd
    • & background
    • $ support varible: echo “.. $VAR”

1. 测试结果

先上结果 (。・∀・)ノ
result

2. 大致框架

首先可以大致写出框架: 打印提示符, 解析命令, 执行内置命令, 执行外部命令. 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//by osh助教
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>

int main() {
/* 输入的命令行 */
char cmd[256];
/* 命令行拆解成的各部分,以空指针结尾 */
char *args[128];
while (1) {
/* 提示符 */
printf("# ");
fflush(stdin);
fgets(cmd, 256, stdin);
/* 清理结尾的换行符 */
int i;
for (i = 0; cmd[i] != '\n'; i++)
;
cmd[i] = '\0';
/* 拆解命令行 */
args[0] = cmd;
for (i = 0; *args[i]; i++)
for (args[i+1] = args[i] + 1; *args[i+1]; args[i+1]++)
if (*args[i+1] == ' ') {
*args[i+1] = '\0';
args[i+1]++;
break;
}
args[i] = NULL;

/* 没有输入命令 */
if (!args[0])
continue;

/* 内建命令 */
if (strcmp(args[0], "cd") == 0) {
if (args[1])
chdir(args[1]);
continue;
}
if (strcmp(args[0], "pwd") == 0) {
char wd[4096];
puts(getcwd(wd, 4096));
continue;
}
if (strcmp(args[0], "exit") == 0)
return 0;

/* 外部命令 */
pid_t pid = fork();
if (pid == 0) {
/* 子进程 */
execvp(args[0], args);
/* execvp失败 */
return 255;
}
/* 父进程 */
wait(NULL);
}
}

上面的大致框架是助教写的示例, 下面我将一步步的改进, 我的完整代码见文末

3. 全局变量说明

3.1. cmdStr

是用来接收输入的一个字符串数组

3.2. cmdNum, varNum

cmdNum记录 以 ; 分开的命令数目,
varNum 记录 每条命令中的变量 $ 的个数

3.3. envVar

存储环境变量

3.4. cmd 结构

1
2
3
4
5
6
7
8
9
struct cmd{
struct cmd * next;
int begin,end; // pos in cmdStr
int argc;
char lredir,rredir; //0:no redirect 1 <,> ; 2 >>
char toFile[MAX_PATH_LENGTH],fromFile[MAX_PATH_LENGTH]; // redirect file path
char *args[MAX_ARG_NUM];
char bgExec; //failExec
};

next 是用来指向管道的下一次指令, 而全局变量 cmdinfo 数组定义如下

1
struct cmd cmdinfo[MAX_CMD_NUM];

是用来存放以 ; 分开的多条指令.

4. 解析命令字符串

上面的大致框架简单实现中, 不够强壮, 比如命令字符串中不能连续多个空格等等. 所以在最后面的代码中, 重新实现解析命令字符串, 就是 parseArgs函数, 限于篇幅, 代码见文末.

这些函数解析命令字符串, 能支持多个空格, 支持多行输入, 支持了变量$, 支持引号',", 同时为重定向 <,>,<<,以及 管道 |,做好准备

5. 多条命令的解析—;

parseCmds 函数解析多行输入,处理多个空格,
\t 符号换为空格, 将多行命令通过命令结点形成链表.
在这个函数中, 也解析后台运行&符号, 如果有的话, 就设置命令头结点 的 head->bgEXec

6. 实现后台运行—-&

这只需在创建子进程的实现, 是否让父进程 wait
这在 main 函数中可以看到

1
if(!pcmd->bgExec)wait(NULL);

7. 处理变量—$

parseCmds 函数 解析命令字符串时, 调用 handleVar 函数解析变量, 其工作是指示是否有变量, 如果有就解析记录下变量的名字

8. 内建命令

对于内建命令, 比如 ls, pwd, exit, env, unset 可以直接执行
在代码中, 内建命令的实现都在 execInner 函数中, 如果不是内建命令, 则返回1, 然后会调用执行外部命令的函数 execOuter

8.1. 实现 ls

1
2
3
4
5
6
7
8
9
10
11
 int LS(char *path){
DIR *dirp;
struct dirent d,*dp = &d;
dirp = opendir(path);
int ct=0;
while((dp=readdir(dirp))!=NULL){
printf("%s\n",dp->d_name);//,++ct%5==0?'\n':'');
}
closedir(dirp);
return 0;
}

8.2. 实现 cd

pcmd->args[1] 是目的路径的指针

1
2
3
4
5
6
7
8
9
10
struct stat st;
if (pcmd->args[1]){
stat(pcmd->args[1],&st);
if (S_ISDIR(st.st_mode))
chdir(pcmd->args[1]);
else{
printf("[Error]: cd '%s': No such directory\n",pcmd->args[1]);
return -1;
}
}

8.3. 实现 pwd

1
printf("%s\n",getcwd(pcmd->args[1] , MAX_PATH_LENGTH));

8.4. 实现unset

unsetenv 调用, pcmd->args[i]是命令的各个参数的指针, 注意从1开始, 第0个参数是命令程序自己

1
for(int i=1;i<pcmd->argc;++i)unsetenv(pcmd->args[i]);

8.5. 实现 export

1
2
3
4
5
6
7
for(int i=1;i<pcmd->argc;++i){  //putenv( pcmd->args[i]);
char *val,*p;
for(p = pcmd->args[i];*p!='=';++p);
*p='\0';
val = p+1;
setenv(pcmd->args[i],val,1);
}

9. 实现重定向与管道— <,>,>>,|

首先要知道一些关于linux文件I/O的知识, 可以看我这篇笔记

重定向的I/O 以及 管道的I/O, 我都放在 setIO 函数中处理,如下.
这个函数接受的参数包括一个命令指针 pcmd (以;分隔的, 包括管道中的命令), 以及 一个输入文件描述符rfd,一个输出文件描述符wfd.

9.1. 文件重定向

如果这条命令中( pcmd->rredir输出重定向)
/( pcmd->lredir 输入重定向) 不为0, 就打开重定向的文件得到其文件描述符, 然后将标准 输出/输入文件描述符关闭, 再复制(用的dup2)到此文件描述符, 注意最后用完 此文件描述符 要用close关闭它.

9.2. 管道重定向

分别检查 文件描述符参数 是否 是标准输入,输出, 如果不是, 说明传递的是管道, 新的文件描述符, 就将相应的 标准输入/输出 关闭 ,再复制到 rfd/wfd, 最后close rfd/wfd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void setIO(struct cmd *pcmd,int rfd,int wfd){
/* settle file and pipe redirect */
if(pcmd->rredir>0){ // >, >>
int flag ;
if(pcmd->rredir==1)flag=O_WRONLY|O_TRUNC|O_CREAT; // > note: trunc is necessary!!!
else flag=O_WRONLY|O_APPEND|O_CREAT; // >>
int wport = open(pcmd->toFile,flag);
dup2(wport,STDOUT_FILENO);
close(wport);
}
if(pcmd->lredir>0){ //<, <<
int rport = open(pcmd->fromFile,O_RDONLY);
dup2(rport,STDIN_FILENO);
close(rport);
}

/* pipe */
if(rfd!=STDIN_FILENO){
dup2(rfd,STDIN_FILENO);
close(rfd);
}
if(wfd!=STDOUT_FILENO){
dup2(wfd,STDOUT_FILENO);
close(wfd);
}
}

10. 外部命令

实现的函数是 execOuter, 里面包括了重定向, 管道, 下面再介绍
对于外部命令, 应该 fork 一个 子进程, 让后让程序在子进程执行并返回, 可以使用 exec 家族的函数, 它会自动调用相应程序文, 件运行(忘了在哪个目录了��), 我用的 execvp 函数

如果当前命令 的 nextNULL, 即没有下一条管道命令, 那么直接将标准文件描述符传给 setIO 处理好文件 IO, 然后调用execvp 执行外部命令即可

如果不为NULL, 说明有管道, 建立管道 , 用fork来新建子进程 执行管道命令, 这时传递到 setIO 函数的 对应 是 管道文件描述符的 输入输出, 然后如果有多个管道, 可以递归地调用 execOuter函数, 如 cmd1 | cmd2 | cmd3…
我的实现是子进程执行 cmd1, 然后 将 cmd2 | cmd3 做为一个新命令传给 execOuter递归执行, 由于是用链表将各管道命令连起来的, 所以 直接传递 pmcd->next 即可, 非常方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int execOuter(struct cmd * pcmd){
if(!pcmd->next){
setIO(pcmd,STDIN_FILENO,STDOUT_FILENO);
execvp(pcmd->args[0],pcmd->args);
}
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid<0){
Error(FORK_ERROR);
}else if (pid==0){
close(fd[0]);
setIO(pcmd,STDIN_FILENO,fd[1]);
execvp(pcmd->args[0],pcmd->args);
Error(EXEC_ERROR);
}else{
wait(NULL);
pcmd = pcmd->next; //notice
close(fd[1]);
setIO(pcmd,fd[0],STDOUT_FILENO);
execOuter(pcmd);
}
}

11. 其他

一些初始化, 错误处理等代码, 我就不再介绍, 可以直接看代码, 代码中有注释, 很容易看懂

12. 完整代码

访问 github

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
/************************************************************************
> File Name: init.c
> Author: mbinary
> Mail: zhuheqin1@gmail.com
> Blog: https://mbinary.github.io
> Created Time: 2018-04-15 11:18
> Function:
implemented some shell cmds and features;
including:
cmds: pwd,ls, cd ,cat, env, export , unset,
features:$ \ | <>>> ; & " ' quote handle \t redundent blank
************************************************************************/

#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <malloc.h>

#define MAX_CMD_LENGTH 255
#define MAX_PATH_LENGTH 255
#define MAX_BUF_SIZE 4096
#define MAX_ARG_NUM 50
#define MAX_VAR_NUM 50
#define MAX_CMD_NUM 10
#define MAX_VAR_LENGTH 500

#define FORK_ERROR 2
#define EXEC_ERROR 3

struct cmd{
struct cmd * next;
int begin,end; // pos in cmdStr
int argc;
char lredir,rredir; ////0:no redirect 1 <,> ; 2 >>
char toFile[MAX_PATH_LENGTH],fromFile[MAX_PATH_LENGTH]; // redirect file path
char *args[MAX_ARG_NUM];
char bgExec; //failExec
};

struct cmd cmdinfo[MAX_CMD_NUM];
char cmdStr[MAX_CMD_LENGTH];
int cmdNum,varNum;
char envVar[MAX_VAR_NUM][MAX_PATH_LENGTH];


void Error(int );
void debug(struct cmd*);
void init(struct cmd*);
void setIO(struct cmd*,int ,int );
int getInput();
int parseCmds(int);
int handleVar(struct cmd *,int);
int getItem(char *,char *,int);
int parseArgs();
int execInner(struct cmd*);
int execOuter(struct cmd*);


int main(){
while (1){
cmdNum = varNum = 0;
printf("# ");
fflush(stdin);
int n = getInput();
if(n<=0)continue;
parseCmds(n);
if(parseArgs()<0)continue;
for(int i=0;i<cmdNum;++i){
struct cmd *pcmd=cmdinfo+i, * tmp;
//debug(pcmd);
//pcmd = reverse(pcmd);
int status = execInner(pcmd);
if(status==1){
/*notice!!! Use child proc to execute outer cmd,
bacause exec funcs won't return when successfully execed. */
pid_t pid = fork();
if(pid==0)execOuter(pcmd);
else if(pid<0)Error(FORK_ERROR);
if(!pcmd->bgExec)wait(NULL); //background exec
/* free malloced piep-cmd-node,
and the first one is static , no need to free; */
pcmd=pcmd->next;
while(pcmd){
tmp = pcmd->next;
free(pcmd);
pcmd=tmp;
}
}
}

}
return 0;
}


/* funcs implementation */
void init(struct cmd *pcmd){
pcmd->bgExec=0;
pcmd->argc=0;
pcmd->lredir=pcmd->rredir=0;
pcmd->next = NULL;
pcmd->begin=pcmd->end=-1;
/* // notice!!! Avoid using resudent args */
for(int i=0;i<MAX_ARG_NUM;++i)pcmd->args[i]=NULL;
}

void Error(int n){
switch(n){
case FORK_ERROR:printf("fork error\n");break;
case EXEC_ERROR:printf("exec error\n");break;
truetruedefault:printf("Error, exit ...\n");
}
exit(1);
}



int getInput(){
/* multi line input */
int pCmdStr=0,cur;
char newline = 1;
while(newline){
cur = MAX_CMD_LENGTH-pCmdStr;
if(cur<=0){
printf("[Error]: You cmdStr is too long to exec.\n");
return -1;// return -1 if cmdStr size is bigger than LENGTH
}
fgets(cmdStr+pCmdStr,cur,stdin);
newline = 0;
while(1){
if(cmdStr[pCmdStr]=='\\'&&cmdStr[pCmdStr+1]=='\n'){
newline=1;
cmdStr[pCmdStr++]='\0';
break;
}
else if(cmdStr[pCmdStr]=='\n'){
break;
}
++pCmdStr;
}
}
return pCmdStr;
}

int parseCmds(int n){
/* clean the cmdStr and get pos of each cmd in the cmdStr (OoO) */
char beginCmd=0;
struct cmd * head; // use head cmd to mark background.
for( int i=0;i<=n;++i){
switch(cmdStr[i]){
case '&':{
if(cmdStr[i+1]=='\n'||cmdStr[i+1]==';'){
cmdStr[i]=' ';
head->bgExec=1;
}
}
truetruetruecase '\t':cmdStr[i]=' ';break;
case ';':{//including ';' a new cmdStr
beginCmd = 0;
cmdStr[i]='\0';
cmdinfo[cmdNum++].end=i;
break;
}
case '\n':{
cmdStr[i]='\0';
cmdinfo[cmdNum++].end =i;
return 0;
}
case ' ':break;
default:if(!beginCmd){
beginCmd=1;
head = cmdinfo+cmdNum;
cmdinfo[cmdNum].begin = i;
}
}
}
}

int getItem(char *dst,char*src, int p){
/* get redirect file path from the cmdStr */
int ct=0;
while(src[++p]==' ');
if(src[p]=='\n')return -1; //no file
char c;
while(c=dst[ct]=src[p]){
if(c==' '||c=='|'||c=='<'||c=='>'||c=='\n')break;
++ct,++p;
}
dst[ct]='\0';
return p-1;
}

int handleVar(struct cmd *pcmd,int n){
char * arg = pcmd->args[n];
int p_arg=0,p_var=0;
while(arg[p_arg]){
if((arg[p_arg]=='$')&&(arg[p_arg-1]!='\\')){
if(arg[p_arg+1]=='{')p_arg+=2;
else p_arg+=1;
char *tmp=&envVar[varNum][p_var];
int ct=0;
while(tmp[ct]=arg[p_arg]){
if(tmp[ct]=='}'){
++p_arg;
break;
}
if(tmp[ct]==' '||tmp[ct]=='\n'||tmp[ct]=='\0')break;
++ct,++p_arg;
}
tmp[ct]='\0';
tmp = getenv(tmp);
for(int i=0;envVar[varNum][p_var++]=tmp[i++];);
p_var-=1; //necessary
}
else envVar[varNum][p_var++]=arg[p_arg++];
}
envVar[varNum][p_var]='\0';
pcmd->args[n] = envVar[varNum++];
return 0;
}

int parseArgs(){
/* get args of each cmd and create cmd-node seperated by pipe */
char beginItem=0,beginQuote=0,beginDoubleQuote=0,hasVar=0,c;
trueint begin,end;
truestruct cmd* pcmd;
for(int p=0;p<cmdNum;++p){
truetrueif(beginQuote||beginItem||beginDoubleQuote){
truetruetruereturn -1; // wrong cmdStr
truetrue}
pcmd=&cmdinfo[p];
begin = pcmd->begin,end = pcmd->end;
init(pcmd);// initalize
for(int i=begin;i<end;++i){
c = cmdStr[i];
truetruetrueif((c=='\"')&&(cmdStr[i-1]!='\\'&&(!beginQuote))){
truetruetruetrueif(beginDoubleQuote){
truetruetruetruetruecmdStr[i]=beginDoubleQuote=beginItem=0;
if(hasVar){
hasVar=0;
handleVar(pcmd,pcmd->argc-1); //note that is argc-1, not argc
}
}else{
truetruetruetruetruebeginDoubleQuote=1;
truetruetruetruetruepcmd->args[pcmd->argc++]=cmdStr+i+1;
truetruetruetrue}
continue;
truetruetrue}else if(beginDoubleQuote){
if((c=='$') &&(cmdStr[i-1]!='\\')&&(!hasVar))hasVar=1;
continue;
}

if((c=='\'')&&(cmdStr[i-1]!='\\')){
if(beginQuote){
truetruetruetruetruecmdStr[i]=beginQuote=beginItem=0;
}else{
beginQuote=1;
pcmd->args[pcmd->argc++]=cmdStr+i+1;
}
continue;
}else if(beginQuote) continue;


if(c=='<'||c=='>'||c=='|'){
if(beginItem)beginItem=0;
cmdStr[i]='\0';
}
if(c=='<'){
if(cmdStr[i+1]=='<'){
pcmd->lredir+=2; //<<
cmdStr[i+1]=' ';
}else{
pcmd->lredir+=1; //<
}
int tmp = getItem(pcmd->fromFile,cmdStr,i);
if(tmp>0)i = tmp;
}else if(c=='>'){
if(cmdStr[i+1]=='>'){
pcmd->rredir+=2; //>>
cmdStr[i+1]=' ';
}else{
pcmd->rredir+=1; //>
}
int tmp = getItem(pcmd->toFile,cmdStr,i);
if(tmp>0)i = tmp;
}else if (c=='|'){
/*when encountering pipe | , create new cmd node chained after the fommer one */
pcmd->end = i;
pcmd->next = (struct cmd*)malloc(sizeof(struct cmd));
pcmd = pcmd->next;
init(pcmd);
}else if(c==' '||c=='\0'){
if(beginItem){
beginItem=0;
cmdStr[i]='\0';
}
}else{
if(pcmd->begin==-1)pcmd->begin=i;
if(!beginItem){
beginItem=1;
if((c=='$') &&(cmdStr[i-1]!='\\')&&(!hasVar))hasVar=1;
pcmd->args[pcmd->argc++]=cmdStr+i;
}
}

if(hasVar){
hasVar=0;
handleVar(pcmd,pcmd->argc-1); //note that is argc-1, not argc
}
}
pcmd->end=end;
//printf("%dfrom:%s %dto:%s\n",pcmd->lredir,pcmd->fromFile,pcmd->rredir,pcmd->toFile);
}
}

int execInner(struct cmd* pcmd){
/*if inner cmd, {exec, return 0} else return 1 */
if (!pcmd->args[0])
return 0;
if (strcmp(pcmd->args[0], "cd") == 0) {
struct stat st;
if (pcmd->args[1]){
stat(pcmd->args[1],&st);
if (S_ISDIR(st.st_mode))
chdir(pcmd->args[1]);
else{
printf("[Error]: cd '%s': No such directory\n",pcmd->args[1]);
return -1;
}
}
return 0;
}
if (strcmp(pcmd->args[0], "pwd") == 0) {
printf("%s\n",getcwd(pcmd->args[1] , MAX_PATH_LENGTH));
return 0;
}
if (strcmp(pcmd->args[0], "unset") == 0) {
for(int i=1;i<pcmd->argc;++i)unsetenv(pcmd->args[i]);
return 0;
}
if (strcmp(pcmd->args[0], "export") == 0) {
for(int i=1;i<pcmd->argc;++i){ //putenv(pcmd->args[i]);
char *val,*p;
for(p = pcmd->args[i];*p!='=';++p);
*p='\0';
val = p+1;
setenv(pcmd->args[i],val,1);
}
return 0;
}
if (strcmp(pcmd->args[0], "exit") == 0)
exit(0);
return 1;
}

void setIO(struct cmd *pcmd,int rfd,int wfd){
/* settle file redirect */
if(pcmd->rredir>0){ // >, >>
int flag ;
if(pcmd->rredir==1)flag=O_WRONLY|O_TRUNC|O_CREAT; // > note: trunc is necessary!!!
else flag=O_WRONLY|O_APPEND|O_CREAT; //>>
int wport = open(pcmd->toFile,flag);
dup2(wport,STDOUT_FILENO);
close(wport);
}
if(pcmd->lredir>0){ //<, <<
int rport = open(pcmd->fromFile,O_RDONLY);
dup2(rport,STDIN_FILENO);
close(rport);
}

/* pipe */
if(rfd!=STDIN_FILENO){
dup2(rfd,STDIN_FILENO);
close(rfd);
}
if(wfd!=STDOUT_FILENO){
dup2(wfd,STDOUT_FILENO);
close(wfd);
}
}

int execOuter(struct cmd * pcmd){
if(!pcmd->next){
setIO(pcmd,STDIN_FILENO,STDOUT_FILENO);
execvp(pcmd->args[0],pcmd->args);
}
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid<0){
Error(FORK_ERROR);
}else if (pid==0){
close(fd[0]);
setIO(pcmd,STDIN_FILENO,fd[1]);
execvp(pcmd->args[0],pcmd->args);
Error(EXEC_ERROR);
}else{
wait(NULL);
pcmd = pcmd->next; //notice
close(fd[1]);
setIO(pcmd,fd[0],STDOUT_FILENO);
execOuter(pcmd);
}
}

本文结束感谢阅读