Preface
上一篇我們實現了一個最簡單的shell,並且這個shell只是去執行了bash的指令,那麼我們如果要去實現所有的命令需要怎麼做呢?比如ls。
首先,我們就應該想到解析引數,因為只要解析了引數我們就能呼叫exec函式去執行命令了。
一般來講,
int mian(argc,**argv)
這是最常見的傳入命令列引數的方式,那麼問題來了,argv是怎麼樣從string解析出來的呢?需要考慮很多魯棒性的問題,去空格,取命令等等。下面我們就先來實現怎麼取解析輸入命令吧。
解析輸入命令
這裡要好好利用strtok這個函式,可以很方便的切分 char[] 型別的字串。
我從 stackoverflow 的回答裡找到很多巧妙的辦法 傳送門
我認為用下面這種方法最簡潔並易於理解。
enum { kMaxArgs = 64 };
int argc = 0;
char *argv[kMaxArgs];
// 解析命令成 (argc,**argv)
int parse_para(char commandLine[]) {
char *p2;
p2 = strtok(commandLine, " ");
while (p2 && argc < kMaxArgs-1)
{
printf("%s
",p2);
argv[argc++] = p2;
p2 = strtok(0, " ");
}
argv[argc] = 0;
}
其實個人更喜歡 c++ 的做法
#include <vector>
#include <string>
#include <sstream>
std::string cmd = "mycommand arg1 arg2";
std::istringstream ss(cmd);
std::string arg;
std::list<std::string> ls;
std::vector<char*> v;
while (ss >> arg)
{
ls.push_back(arg);
v.push_back(const_cast<char*>(ls.back().c_str()));
}
v.push_back(0); // need terminating null pointer
execv(v[0], &v[0]);
不管哪種方式,這樣我們每次輸入的string就可以轉化成argc和**argv了(全域性變數)
接下來,介紹一個函式 —> getopt
man 3 getopt 可以獲得一個例子
getopt()
The following trivial example program uses getopt() to handle two program options: -n,
with no associated value; and -t val, which expects an associated value.
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int
main(int argc, char *argv[])
{
int flags, opt;
int nsecs, tfnd;
nsecs = 0;
tfnd = 0;
flags = 0;
while ((opt = getopt(argc, argv, "nt:")) != -1) {
switch (opt) {
case `n`:
flags = 1;
break;
case `t`:
nsecs = atoi(optarg);
tfnd = 1;
break;
default: /* `?` */
fprintf(stderr, "Usage: %s [-t nsecs] [-n] name
",
argv[0]);
exit(EXIT_FAILURE);
}
}
printf("flags=%d; tfnd=%d; nsecs=%d; optind=%d
",
flags, tfnd, nsecs, optind);
if (optind >= argc) {
fprintf(stderr, "Expected argument after options
");
exit(EXIT_FAILURE);
}
printf("name argument = %s
", argv[optind]);
/* Other code omitted */
exit(EXIT_SUCCESS);
}
ok~到此,我們可以解析引數了,那麼下一步就是要執行命令,在這裡,不得不去介紹Unix的exec函式族,8.10 函式exec詳細講解了。
執行命令
8.3節曾提到用fork函式建立新的子程式後,子程式往往要呼叫一種exec函式以執行另一個程式。當程式呼叫一種exec函式時,該程式執行的程式完全替代為新程式。
因為呼叫exec並不建立新程式,所以前後的程式ID並未改變。exec只是用磁碟上的一個新程式替代了當前程式的正文段,資料段,堆段和棧段。
一共有7個不同的exec函式。
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);
int execvp(cosnt char *filename, char *const argv[]);
int fexecve(int fd,char *const argv[],char *const envp[]);
7個函式的返回值:若出錯則返回-1,若成功則沒有返回值
在APUE中,解釋好長的一段,主要集中了三種不同的區別:
-
第一個區別是前4個函式取路徑名作為引數,後兩個函式取檔名作為引數,最後一個取檔案描述符作為引數。
-
如果filename中包含/,則就將其視為路徑名。
-
否則就按照PATH環境變數,在它所指定的各目錄中搜尋可執行檔案。
PATH變數包含了一張目錄表(成為路徑字首): PATH=/bin:/usr/bin:/usr/local/bin:.
如果 execlp或者execvp使用路徑字首中的一個找到了一個可執行檔案,但是該檔案不是由連線編譯器產生的可執行檔案,則就認為該檔案是一個shell指令碼,試著用/bin/sh去呼叫它。
fexecve函式引數是檔案描述符,這個很重要,因為是檔案描述符,所以就可以無競爭地執行該檔案。否則,擁有特權的惡意使用者可以去篡改該程式。(這裡我的理解),具體是一個TOCTTOU的問題3.3節 TOCTTOU: 基本思想:如果有兩個基於檔案的函式呼叫,其中第二個呼叫依賴於第一個呼叫的結果,那麼程式就是脆弱的。 因為兩個呼叫並不是原子操作,在兩個函式呼叫之間檔案可能改變了,這樣也就造成了第一個呼叫的結果不再有效。 檔案系統名稱空間中的TOCTTOU錯誤通常處理的就是那些顛覆檔案系統許可權的小把戲,這些小把戲通過騙取特權程式降低特權檔案的許可權控制或者讓特權檔案開啟一個安全漏洞等方式進行。
-
-
第二區別與參數列的傳遞有關。(不細說了)
-
最後一個區別與向新程式傳遞環境表有關。
通常,一個程式允許將其環境傳播給其子程式,但也有時有這種情況,程式想要為子程式制定某一個確定的環境,比如初始化一個新登入的shell時,login程式通常會建立一個之定義少數幾個變數的特殊環境,而在我們登入時,可以通過shell啟動檔案,將其他變數加到環境中去。
其實還有更加詳細的分析,但是我也不提太多了,因為我們的目標是星辰大海,不可因小失多。其實我一直認為學習這種大部頭的方法就是,你先找定一個方向,比如我要實現一個Jas-shell(我自己取的名 :)),然後利用這本書的知識不斷去完善我的shell,在這其中,我不能面面俱到,細緻入微,但求大刀闊斧,直指前方。當未來我實現了,剛好也大概過了一遍這本書,我會回頭慢慢咀嚼細節,然後update我的作品。
不小心廢話了一下,哈哈,半桶水叮噹響,各位看客一笑了之~
好了,下面我貼出一個例項,就是在我們第一章實現的基本shell上改的,至於裡面用到的imitate_ls的實現,我放到下一章講~
其中的 /home/jasperyang/CLionProjects/Jas-shell/imitate_ls 是我實現的ls沒程式碼貼出來,大家耐心等我下一章~或者你們可以自己實現。
//
// Created by jasperyang on 17-6-6.
//
#include "apue.h"
#include <sys/wait.h>
#include "myerr.h"
static void sig_int(int); /* our signal-catching function */
static int parse_para(char commandLine[]);
enum { kMaxArgs = 64 };
int argc=0; //命令列引數個數
char *argv[kMaxArgs]; //命令列引數
int main(void) {
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
if(signal(SIGINT,sig_int)==SIG_ERR)
err_sys("signal error");
printf("%% "); /* print prompt (printf requires %% to print %) */
while(fgets(buf,MAXLINE,stdin) != NULL) {
if(buf[strlen(buf) -1] == `
`){
buf[strlen(buf)-1]=0; /* replace newline with null */
}
if((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0){ /* child */
argc = 0;
parse_para(buf);
printf("%s
",argv[0]);
if(!strcmp(argv[0],"ls")) {
if (execv("/home/jasperyang/CLionProjects/Jas-shell/imitate_ls", argv) < 0) {
printf("execv error: %s
", strerror(errno));
exit(-1);
}
}
else {
err_ret("couldn`t execute: %s", buf);
}
exit(127);
}
/* parent */
if((pid = waitpid(pid,&status,0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
//中斷訊號
void sig_int(int signo) {
printf("interrupt
%% ");
}
// 解析命令成 (argc,**argv)
int parse_para(char commandLine[]) {
char *p2;
p2 = strtok(commandLine, " ");
while (p2 && argc < kMaxArgs-1)
{
printf("%s
",p2);
argv[argc++] = p2;
p2 = strtok(0, " ");
}
argv[argc] = 0;
}
休息一下,下一章見~