笨辦法學(xué)C 練習(xí)26:編寫第一個(gè)真正的程序

練習(xí)26:編寫第一個(gè)真正的程序

原文:Exercise 26: Write A First Real Program

譯者:飛龍

這本書你已經(jīng)完成一半了上岗,所以你需要做一個(gè)期中檢測(cè)。期中檢測(cè)中你需要重新構(gòu)建一個(gè)我特地為本書編寫的軟件蕴坪,叫做devpkg肴掷。隨后你需要以一些方式擴(kuò)展它,并且通過(guò)編寫一些單元測(cè)試來(lái)改進(jìn)代碼辞嗡。

我在一些你需要完成的練習(xí)之前編寫了這個(gè)練習(xí)捆等。如果你現(xiàn)在嘗試這個(gè)練習(xí),記住軟件可能會(huì)含有一些bug续室,你可能由于我的錯(cuò)誤會(huì)產(chǎn)生一些問(wèn)題栋烤,也可能不知道需要什么來(lái)完成它。如果這樣的話挺狰,通過(guò)help@learncodethehardway.org來(lái)告訴我明郭,之后等待我寫完其它練習(xí)。

什么是devpkg丰泊?

devpkg是一個(gè)簡(jiǎn)單的C程序薯定,可以用于安裝其它軟件。我特地為本書編寫了它瞳购,作為一種方式來(lái)教你真正的軟件是如何構(gòu)建的话侄,以及如何復(fù)用他人的庫(kù)。它使用了一個(gè)叫做Apache可移植運(yùn)行時(shí)(APR)的庫(kù),其中含有許多工作跨平臺(tái)的便利的C函數(shù)年堆,包括Windows吞杭。此外稚矿,它只是從互聯(lián)網(wǎng)(或本地文件)抓取代碼植捎,并且執(zhí)行通常的./configure ; make ; make install命令,每個(gè)程序員都用到過(guò)庶橱。

這個(gè)練習(xí)中痒蓬,你的目標(biāo)是從源碼構(gòu)建devpkg童擎,完成我提供的每個(gè)挑戰(zhàn),并且使用源碼來(lái)理解devpkg做了什么和為什么這樣做攻晒。

我們打算創(chuàng)建什么

我們打算創(chuàng)建一個(gè)具有三個(gè)命令的工具:

devpkg -S

在電腦上安裝新的軟件顾复。

devpkg -I

從URL安裝軟件。

devpkg -L

列出安裝的所有軟件鲁捏。

devpkg -F

為手動(dòng)構(gòu)建抓取源代碼捕透。

devpkg -B

構(gòu)建所抓取的源碼代碼并且安裝它,即使它已經(jīng)安裝了碴萧。

我們想讓devpkg能夠接受幾乎任何URL乙嘀,判斷項(xiàng)目的類型,下載破喻,安裝虎谢,以及注冊(cè)已經(jīng)安裝的軟件。我們也希望它能夠處理一個(gè)簡(jiǎn)單的依賴列表曹质,以便它能夠安裝項(xiàng)目所需的所有軟件婴噩。

設(shè)計(jì)

為了完成這一目標(biāo),devpkg具有非常簡(jiǎn)單的設(shè)計(jì):

使用外部命令

大多數(shù)工作都是通過(guò)類似于curl羽德、gittar的外部命令完成的几莽。這樣減少了devpkg所需的代碼量。

簡(jiǎn)單的文件數(shù)據(jù)庫(kù)

你可以輕易使它變得很復(fù)雜宅静,但是一開(kāi)始你需要完成一個(gè)簡(jiǎn)單的文件數(shù)據(jù)庫(kù)章蚣,位于/usr/local/.devpkg/db,來(lái)跟蹤已安裝的軟件姨夹。

/usr/local

同樣你可以使它更高級(jí)纤垂,但是對(duì)于初學(xué)者來(lái)說(shuō),假設(shè)項(xiàng)目始終位于/usr/local中磷账,它是愛(ài)多數(shù)Unix軟件的標(biāo)準(zhǔn)安裝目錄峭沦。

configure; make; make install

假設(shè)大多數(shù)軟件可以通過(guò)configure; make; make install來(lái)安裝,也許configure是可選的逃糟。如果軟件不能通過(guò)這種方式安裝吼鱼,要么提供某種方式來(lái)修改命令蓬豁,要么devpkg就可以無(wú)視它。

用戶可以root

我們假設(shè)用于可以使用sudo來(lái)提升至root權(quán)限菇肃,除非他們直到最后才想root庆尘。

這會(huì)使我們的程序像當(dāng)初設(shè)想的一樣簡(jiǎn)單,并且對(duì)于它的功能來(lái)說(shuō)已經(jīng)足夠了巷送。之后你可以進(jìn)一步修改它。

Apache 可移植運(yùn)行時(shí)

你需要做的另外一件事情就是使用Apache可移植運(yùn)行時(shí)(APR)來(lái)未完成這個(gè)練習(xí)獲得一個(gè)可移植的工具集矛辕。APR并不是必要的笑跛,你也可以不用它,但是你需要寫的代碼就會(huì)非常多聊品。我現(xiàn)在強(qiáng)制你使用APR飞蹂,使你能夠熟悉鏈接和使用其他的庫(kù)。最后翻屈,APR也能在Windows上工作陈哑,所以你可以把它遷移到許多其它平臺(tái)上。

你應(yīng)該獲取apr-1.4.5apr-util-1.3的庫(kù)伸眶,以及瀏覽在apr.apache.org主站上的文檔惊窖。

下面是一個(gè)ShellScript,用于安裝所需的所有庫(kù)厘贼。你應(yīng)該手動(dòng)將它寫到一個(gè)文件中界酒,之后運(yùn)行它直到APR安裝好并且沒(méi)有任何錯(cuò)誤。

set -e

# go somewhere safe
cd /tmp

# get the source to base APR 1.4.6
curl -L -O http://archive.apache.org/dist/apr/apr-1.4.6.tar.gz

# extract it and go into the source
tar -xzvf apr-1.4.6.tar.gz
cd apr-1.4.6

# configure, make, make install
./configure
make
sudo make install

# reset and cleanup
cd /tmp
rm -rf apr-1.4.6 apr-1.4.6.tar.gz

# do the same with apr-util
curl -L -O http://archive.apache.org/dist/apr/apr-util-1.4.1.tar.gz

# extract
tar -xzvf apr-util-1.4.1.tar.gz
cd apr-util-1.4.1

# configure, make, make install
./configure --with-apr=/usr/local/apr
# you need that extra parameter to configure because
# apr-util can't really find it because...who knows.

make
sudo make install

#cleanup
cd /tmp
rm -rf apr-util-1.4.1* apr-1.4.6*

我希望你輸入這個(gè)腳本嘴秸,因?yàn)檫@就是devpkg基本上所做的事情毁欣,只是帶有了一些選項(xiàng)和檢查項(xiàng)。實(shí)際上岳掐,你可以使用Shell以更少的代碼來(lái)完成它凭疮,但是這對(duì)于一本C語(yǔ)言的書不是一個(gè)很好的程序。

簡(jiǎn)單運(yùn)行這個(gè)腳本串述,修復(fù)它直到正常工作执解,就完成的所有庫(kù)的安裝,之后你需要完成項(xiàng)目的剩下部分纲酗。

項(xiàng)目布局

你需要?jiǎng)?chuàng)建一些簡(jiǎn)單的項(xiàng)目文件來(lái)起步材鹦。下面是我通常創(chuàng)建一個(gè)新項(xiàng)目的方法:

mkdir devpkg
cd devpkg
touch README Makefile

其它依賴

你應(yīng)該已經(jīng)安裝了APR和APR-util,所以你需要一些更多的文件作為基本的依賴:

  • 練習(xí)20中的dbg.h耕姊。
  • http://bstring.sourceforge.net/下載的bstrlib.hbstrlib.c桶唐。下載.zip文件,解壓并且將這個(gè)兩個(gè)文件拷貝到項(xiàng)目中茉兰。
  • 運(yùn)行make bstrlib.o尤泽,如果這不能正常工作,閱讀下面的“修復(fù)bstring”指南。

在一些平臺(tái)上bstring.c文件會(huì)出現(xiàn)下列錯(cuò)誤:

bstrlib.c:2762: error: expected declaration specifiers or '...' before numeric constant

這是由于作者使用了一個(gè)不好的定義坯约,它在一些平臺(tái)上不能工作熊咽。你需要修改第2759行的#ifdef __GNUC__,并把它改成:

#if defined(__GNUC__) && !defined(__APPLE__)

之后在Mac OSX平臺(tái)上就應(yīng)該能夠正常工作了闹丐。

做完上面這些后横殴,你應(yīng)該有了MakefileREADME卿拴,dbg.h衫仑,bstrlib.hbstrlib.c,并做好了準(zhǔn)備堕花。

Makefile

我們最好從Makefile開(kāi)始文狱,因?yàn)樗谐隽隧?xiàng)目如何構(gòu)建,以及你會(huì)創(chuàng)建哪些源文件缘挽。

PREFIX?=/usr/local
CFLAGS=-g -Wall -I${PREFIX}/apr/include/apr-1  -I${PREFIX}/apr/include/apr-util-1
LDFLAGS=-L${PREFIX}/apr/lib -lapr-1 -pthread -laprutil-1

all: devpkg

devpkg: bstrlib.o db.o shell.o commands.o

install: all
     install -d $(DESTDIR)/$(PREFIX)/bin/
     install devpkg $(DESTDIR)/$(PREFIX)/bin/

clean:
     rm -f *.o
     rm -f devpkg
     rm -rf *.dSYM

比起之前看到過(guò)的瞄崇,這并沒(méi)有什么新東西,除了可能有些奇怪的?=語(yǔ)法壕曼,它表示“如果之前沒(méi)有定義苏研,就將PREFIX設(shè)置為該值”。

如果你使用了最近版本的Ubuntu腮郊,你會(huì)得到apr_off_toff64_t的錯(cuò)誤楣富,之后需要向CFLAGS添加-D_LARGEFILE64_SOURCE=1

所需的另一件事是伴榔,你需要向/etc/ld.conf.so.d/添加/usr/local/apr/lib纹蝴,之后運(yùn)行ldconfig使它能夠選擇正常的庫(kù)。

源文件

我們可以從makefile中看到踪少,devpkg有四個(gè)依賴項(xiàng)塘安,它們是:

bstrlib.o

bstrlib.cbstrlib.o產(chǎn)生,你已經(jīng)將它們引入了援奢。

db.o

db.cdb.h產(chǎn)生兼犯,它包含了一個(gè)小型“數(shù)據(jù)庫(kù)”程序集的代碼。

shell.o

shell.cshell.h產(chǎn)生集漾,包含一些函數(shù)切黔,是類似curl的一些命令運(yùn)行起來(lái)更容易。

commands.o

commands.ccommands.h產(chǎn)生具篇,包含了devpkg所需的所有命令并使它更易用纬霞。

devpkg

它不會(huì)顯式提到,但是它是Makefile在這一部分的目標(biāo)驱显。它由devpkg.c產(chǎn)生诗芜,包含用于整個(gè)程序的main函數(shù)瞳抓。

你的任務(wù)就是創(chuàng)建這些文件,并且輸入代碼并保證正確伏恐。

你讀完這個(gè)描述可能會(huì)想孩哑,“Zed為什么那么聰明,坐著就能設(shè)計(jì)出來(lái)這些文件翠桦?横蜒!”我并不是用我強(qiáng)大的代碼功力魔術(shù)般地把devpkg設(shè)計(jì)成這樣。而是我做了這些:

  • 我編寫了簡(jiǎn)單的README來(lái)獲得如何構(gòu)建項(xiàng)目的靈感销凑。
  • 我創(chuàng)建了一個(gè)簡(jiǎn)單的bash腳本(就像你編寫的那樣)來(lái)理清所需的所有組件丛晌。
  • 我創(chuàng)建了一個(gè).c文件,并且在它上面花了幾天闻鉴,醞釀并想出點(diǎn)子。
  • 接著我編寫并調(diào)試程序茂洒,之后我將這一個(gè)大文件分成四個(gè)文件孟岛。
  • 做完這些之后,我重命名和優(yōu)化了函數(shù)和數(shù)據(jù)結(jié)構(gòu)督勺,使它們?cè)谶壿嬌细懊烙^”渠羞。
  • 最后,使新程序成功并以相同方式工作之后智哀,我添加了一些新的特性次询,比如-F-B選項(xiàng)。

你讀到的這份列表是我打算教給你的瓷叫,但不要認(rèn)為這是我構(gòu)建軟件的通用方法屯吊。有時(shí)候我會(huì)事先知道主題,并且會(huì)做更多的規(guī)劃摹菠。也有時(shí)我會(huì)編寫一份規(guī)劃并將它扔掉盒卸,之后再規(guī)劃更好的版本。它完全取決于我的經(jīng)驗(yàn)告訴我哪個(gè)比較好次氨,或者我的靈感將我?guī)У胶翁帯?/p>

如果你碰到一個(gè)“專家”蔽介,它告訴你只有一個(gè)方法可以解決編程問(wèn)題,那么它在騙你煮寡。要么它們實(shí)際使用了很多策略虹蓄,要么他們并不足夠好。

DB函數(shù)

程序中必須有個(gè)方法來(lái)記錄已經(jīng)安裝的URL幸撕,列出這些URL薇组,并且檢查一些程序是否已安裝以便跳過(guò)。我會(huì)使用一個(gè)簡(jiǎn)單坐儿、扁平化的文件數(shù)據(jù)庫(kù)体箕,以及bstrlib.h专钉。

首先,創(chuàng)建db.h頭文件累铅,以便讓你知道需要實(shí)現(xiàn)什么跃须。

#ifndef _db_h
#define _db_h

#define DB_FILE "/usr/local/.devpkg/db"
#define DB_DIR "/usr/local/.devpkg"


int DB_init();
int DB_list();
int DB_update(const char *url);
int DB_find(const char *url);

#endif

之后實(shí)現(xiàn)db.c中的這些函數(shù),在你編寫它的時(shí)候娃兽,像之前一樣使用make菇民。

#include <unistd.h>
#include <apr_errno.h>
#include <apr_file_io.h>

#include "db.h"
#include "bstrlib.h"
#include "dbg.h"

static FILE *DB_open(const char *path, const char *mode)
{
    return fopen(path, mode);
}


static void DB_close(FILE *db)
{
    fclose(db);
}


static bstring DB_load()
{
    FILE *db = NULL;
    bstring data = NULL;

    db = DB_open(DB_FILE, "r");
    check(db, "Failed to open database: %s", DB_FILE);

    data = bread((bNread)fread, db);
    check(data, "Failed to read from db file: %s", DB_FILE);

    DB_close(db);
    return data;

error:
    if(db) DB_close(db);
    if(data) bdestroy(data);
    return NULL;
}


int DB_update(const char *url)
{
    if(DB_find(url)) {
        log_info("Already recorded as installed: %s", url);
    }

    FILE *db = DB_open(DB_FILE, "a+");
    check(db, "Failed to open DB file: %s", DB_FILE);

    bstring line = bfromcstr(url);
    bconchar(line, '\n');
    int rc = fwrite(line->data, blength(line), 1, db);
    check(rc == 1, "Failed to append to the db.");

    return 0;
error:
    if(db) DB_close(db);
    return -1;
}


int DB_find(const char *url)
{
    bstring data = NULL;
    bstring line = bfromcstr(url);
    int res = -1;

    data = DB_load();
    check(data, "Failed to load: %s", DB_FILE);

    if(binstr(data, 0, line) == BSTR_ERR) {
        res = 0;
    } else {
        res = 1;
    }

error: // fallthrough
    if(data) bdestroy(data);
    if(line) bdestroy(line);

    return res;
}


int DB_init()
{
    apr_pool_t *p = NULL;
    apr_pool_initialize();
    apr_pool_create(&p, NULL);

    if(access(DB_DIR, W_OK | X_OK) == -1) {
        apr_status_t rc = apr_dir_make_recursive(DB_DIR,
                APR_UREAD | APR_UWRITE | APR_UEXECUTE |
                APR_GREAD | APR_GWRITE | APR_GEXECUTE, p);
        check(rc == APR_SUCCESS, "Failed to make database dir: %s", DB_DIR);
    }

    if(access(DB_FILE, W_OK) == -1) {
        FILE *db = DB_open(DB_FILE, "w");
        check(db, "Cannot open database: %s", DB_FILE);
        DB_close(db);
    }

    apr_pool_destroy(p);
    return 0;

error:
    apr_pool_destroy(p);
    return -1;
}


int DB_list()
{
    bstring data = DB_load();
    check(data, "Failed to read load: %s", DB_FILE);

    printf("%s", bdata(data));
    bdestroy(data);
    return 0;

error:
    return -1;
}

挑戰(zhàn)1:代碼復(fù)查

在繼續(xù)之前,仔細(xì)閱讀這些文件的每一行投储,并且確保你以準(zhǔn)確地輸入了它們第练。通過(guò)逐行閱讀代碼來(lái)實(shí)踐它。同時(shí)玛荞,跟蹤每個(gè)函數(shù)調(diào)用娇掏,并且確保你使用了check來(lái)校驗(yàn)返回值。最后勋眯,在APR網(wǎng)站上的文檔婴梧,或者bstrlib.h 或 bstrlib.c的源碼中,查閱每個(gè)你不認(rèn)識(shí)的函數(shù)客蹋。

Shell 函數(shù)

devkpg的一個(gè)關(guān)鍵設(shè)計(jì)是塞蹭,使用類似于curltargit的外部工具來(lái)完成大部分的工作讶坯。我們可以找到在程序內(nèi)部完成這些工作的庫(kù)番电,但是如果我們只是需要這些程序的基本功能,這樣就毫無(wú)意義辆琅。在Unix運(yùn)行其它命令并不丟人漱办。

為了完成這些,我打算使用apr_thread_proc.h函數(shù)來(lái)運(yùn)行程序婉烟,但是我也希望創(chuàng)建一個(gè)簡(jiǎn)單的類“模板”系統(tǒng)洼冻。我會(huì)使用struct Shell,它持有所有運(yùn)行程序所需的信息隅很,但是在參數(shù)中有一些“空位”撞牢,我可以將它們替換成實(shí)際值。

觀察shell.h文件來(lái)了解我會(huì)用到的結(jié)構(gòu)和命令叔营。你可以看到我使用extern來(lái)表明其他的.c文件也能訪問(wèn)到shell.c中定義的變量屋彪。

#ifndef _shell_h
#define _shell_h

#define MAX_COMMAND_ARGS 100

#include <apr_thread_proc.h>

typedef struct Shell {
    const char *dir;
    const char *exe;

    apr_procattr_t *attr;
    apr_proc_t proc;
    apr_exit_why_e exit_why;
    int exit_code;

    const char *args[MAX_COMMAND_ARGS];
} Shell;

int Shell_run(apr_pool_t *p, Shell *cmd);
int Shell_exec(Shell cmd, ...);

extern Shell CLEANUP_SH;
extern Shell GIT_SH;
extern Shell TAR_SH;
extern Shell CURL_SH;
extern Shell CONFIGURE_SH;
extern Shell MAKE_SH;
extern Shell INSTALL_SH;

#endif

確保你已經(jīng)創(chuàng)建了shell.h,并且extern Shell變量的名字和數(shù)量相同绒尊。它們被Shell_runShell_exec函數(shù)用于運(yùn)行命令畜挥。我定義了這兩個(gè)函數(shù),并且在shell.c中創(chuàng)建實(shí)際變量婴谱。

#include "shell.h"
#include "dbg.h"
#include <stdarg.h>

int Shell_exec(Shell template, ...)
{
    apr_pool_t *p = NULL;
    int rc = -1;
    apr_status_t rv = APR_SUCCESS;
    va_list argp;
    const char *key = NULL;
    const char *arg = NULL;
    int i = 0;

    rv = apr_pool_create(&p, NULL);
    check(rv == APR_SUCCESS, "Failed to create pool.");

    va_start(argp, template);

    for(key = va_arg(argp, const char *);
        key != NULL;
        key = va_arg(argp, const char *))
    {
        arg = va_arg(argp, const char *);

        for(i = 0; template.args[i] != NULL; i++) {
            if(strcmp(template.args[i], key) == 0) {
                template.args[i] = arg;
                break; // found it
            }
        }
    }

    rc = Shell_run(p, &template);
    apr_pool_destroy(p);
    va_end(argp);
    return rc;

error:
    if(p) {
        apr_pool_destroy(p);
    }
    return rc;
}

int Shell_run(apr_pool_t *p, Shell *cmd)
{
    apr_procattr_t *attr;
    apr_status_t rv;
    apr_proc_t newproc;

    rv = apr_procattr_create(&attr, p);
    check(rv == APR_SUCCESS, "Failed to create proc attr.");

    rv = apr_procattr_io_set(attr, APR_NO_PIPE, APR_NO_PIPE,
            APR_NO_PIPE);
    check(rv == APR_SUCCESS, "Failed to set IO of command.");

    rv = apr_procattr_dir_set(attr, cmd->dir);
    check(rv == APR_SUCCESS, "Failed to set root to %s", cmd->dir);

    rv = apr_procattr_cmdtype_set(attr, APR_PROGRAM_PATH);
    check(rv == APR_SUCCESS, "Failed to set cmd type.");

    rv = apr_proc_create(&newproc, cmd->exe, cmd->args, NULL, attr, p);
    check(rv == APR_SUCCESS, "Failed to run command.");

    rv = apr_proc_wait(&newproc, &cmd->exit_code, &cmd->exit_why, APR_WAIT);
    check(rv == APR_CHILD_DONE, "Failed to wait.");

    check(cmd->exit_code == 0, "%s exited badly.", cmd->exe);
    check(cmd->exit_why == APR_PROC_EXIT, "%s was killed or crashed", cmd->exe);

    return 0;

error:
    return -1;
}

Shell CLEANUP_SH = {
    .exe = "rm",
    .dir = "/tmp",
    .args = {"rm", "-rf", "/tmp/pkg-build", "/tmp/pkg-src.tar.gz",
        "/tmp/pkg-src.tar.bz2", "/tmp/DEPENDS", NULL}
};

Shell GIT_SH = {
    .dir = "/tmp",
    .exe = "git",
    .args = {"git", "clone", "URL", "pkg-build", NULL}
};

Shell TAR_SH = {
    .dir = "/tmp/pkg-build",
    .exe = "tar",
    .args = {"tar", "-xzf", "FILE", "--strip-components", "1", NULL}
};

Shell CURL_SH = {
    .dir = "/tmp",
    .exe = "curl",
    .args = {"curl", "-L", "-o", "TARGET", "URL", NULL}
};

Shell CONFIGURE_SH = {
    .exe = "./configure",
    .dir = "/tmp/pkg-build",
    .args = {"configure", "OPTS", NULL},
};

Shell MAKE_SH = {
    .exe = "make",
    .dir = "/tmp/pkg-build",
    .args = {"make", "OPTS", NULL}
};

Shell INSTALL_SH = {
    .exe = "sudo",
    .dir = "/tmp/pkg-build",
    .args = {"sudo", "make", "TARGET", NULL}
};

自底向上閱讀shell.c的代碼(這也是常見(jiàn)的C源碼布局)蟹但,你會(huì)看到我創(chuàng)建了實(shí)際的Shell變量躯泰,它在shell.h中以extern修飾。它們雖然在這里华糖,但是也被程序的其它部分使用麦向。這就是創(chuàng)建全局變量的方式,它們可以存在于一個(gè).c文件中客叉,但是可在任何地方使用诵竭。你不應(yīng)該創(chuàng)建很多這類變量,但是它們的確很方便兼搏。

繼續(xù)閱讀代碼卵慰,我們讀到了Shell_run,它是一個(gè)“基”函數(shù)佛呻,只是基于Shell中的東西執(zhí)行命令裳朋。它使用了許多在apr_thread_proc.h中定義的函數(shù),你需要查閱它們的每一個(gè)來(lái)了解工作原理吓著。這就像是一些使用system函數(shù)調(diào)用的代碼一樣鲤嫡,但是它可以讓你控制其他程序的執(zhí)行。例如夜矗,在我們的Shell結(jié)構(gòu)中泛范,存在.dir屬性在運(yùn)行之前強(qiáng)制程序必須在指定目錄中让虐。

最后紊撕,我創(chuàng)建了Shell_exec函數(shù),它是個(gè)變參函數(shù)赡突。你在之前已經(jīng)看到過(guò)了对扶,但是確保你理解了stdarg.h函數(shù)以及如何編寫它們。在下個(gè)挑戰(zhàn)中你需要分析這一函數(shù)惭缰。

挑戰(zhàn)2:分析Shell_exec

為這些文件(以及向挑戰(zhàn)1那樣的完整的代碼復(fù)查)設(shè)置的挑戰(zhàn)是完整分析Shell_exec浪南,并且拆分代碼來(lái)了解工作原理。你應(yīng)該能夠理解每一行代碼漱受,for循環(huán)如何工作络凿,以及參數(shù)如何被替換。

一旦你分析完成昂羡,向struct Shell添加一個(gè)字段絮记,提供需要替代的args變量的數(shù)量。更新所有命令來(lái)接受參數(shù)的正確數(shù)量虐先,隨后增加一個(gè)錯(cuò)誤檢查怨愤,來(lái)確認(rèn)參數(shù)被正確替換,以及在錯(cuò)誤時(shí)退出蛹批。

命令行函數(shù)

現(xiàn)在你需要構(gòu)造正確的命令來(lái)完成功能撰洗。這些命令會(huì)用到APR的函數(shù)篮愉、db.hshell.h來(lái)執(zhí)行下載和構(gòu)建軟件的真正工作。這些文件最為復(fù)雜差导,所以要小心編寫它們试躏。你需要首先編寫commands.h文件,接著在commands.c文件中實(shí)現(xiàn)它的函數(shù)柿汛。

#ifndef _commands_h
#define _commands_h

#include <apr_pools.h>

#define DEPENDS_PATH "/tmp/DEPENDS"
#define TAR_GZ_SRC "/tmp/pkg-src.tar.gz"
#define TAR_BZ2_SRC "/tmp/pkg-src.tar.bz2"
#define BUILD_DIR "/tmp/pkg-build"
#define GIT_PAT "*.git"
#define DEPEND_PAT "*DEPENDS"
#define TAR_GZ_PAT "*.tar.gz"
#define TAR_BZ2_PAT "*.tar.bz2"
#define CONFIG_SCRIPT "/tmp/pkg-build/configure"

enum CommandType {
    COMMAND_NONE, COMMAND_INSTALL, COMMAND_LIST, COMMAND_FETCH,
    COMMAND_INIT, COMMAND_BUILD
};


int Command_fetch(apr_pool_t *p, const char *url, int fetch_only);

int Command_install(apr_pool_t *p, const char *url, const char *configure_opts,
        const char *make_opts, const char *install_opts);

int Command_depends(apr_pool_t *p, const char *path);

int Command_build(apr_pool_t *p, const char *url, const char *configure_opts,
        const char *make_opts, const char *install_opts);

#endif

commands.h中并沒(méi)有很多之前沒(méi)見(jiàn)過(guò)的東西冗酿。你應(yīng)該看到了一些字符串的定義,它們?cè)谌魏蔚胤蕉紩?huì)用到络断。真正的代碼在commands.c中裁替。

#include <apr_uri.h>
#include <apr_fnmatch.h>
#include <unistd.h>

#include "commands.h"
#include "dbg.h"
#include "bstrlib.h"
#include "db.h"
#include "shell.h"


int Command_depends(apr_pool_t *p, const char *path)
{
    FILE *in = NULL;
    bstring line = NULL;

    in = fopen(path, "r");
    check(in != NULL, "Failed to open downloaded depends: %s", path);

    for(line = bgets((bNgetc)fgetc, in, '\n'); line != NULL;
            line = bgets((bNgetc)fgetc, in, '\n'))
    {
        btrimws(line);
        log_info("Processing depends: %s", bdata(line));
        int rc = Command_install(p, bdata(line), NULL, NULL, NULL);
        check(rc == 0, "Failed to install: %s", bdata(line));
        bdestroy(line);
    }

    fclose(in);
    return 0;

error:
    if(line) bdestroy(line);
    if(in) fclose(in);
    return -1;
}

int Command_fetch(apr_pool_t *p, const char *url, int fetch_only)
{
    apr_uri_t info = {.port = 0};
    int rc = 0;
    const char *depends_file = NULL;
    apr_status_t rv = apr_uri_parse(p, url, &info);

    check(rv == APR_SUCCESS, "Failed to parse URL: %s", url);

    if(apr_fnmatch(GIT_PAT, info.path, 0) == APR_SUCCESS) {
        rc = Shell_exec(GIT_SH, "URL", url, NULL);
        check(rc == 0, "git failed.");
    } else if(apr_fnmatch(DEPEND_PAT, info.path, 0) == APR_SUCCESS) {
        check(!fetch_only, "No point in fetching a DEPENDS file.");

        if(info.scheme) {
            depends_file = DEPENDS_PATH;
            rc = Shell_exec(CURL_SH, "URL", url, "TARGET", depends_file, NULL);
            check(rc == 0, "Curl failed.");
        } else {
            depends_file = info.path;
        }

        // recursively process the devpkg list
        log_info("Building according to DEPENDS: %s", url);
        rv = Command_depends(p, depends_file);
        check(rv == 0, "Failed to process the DEPENDS: %s", url);

        // this indicates that nothing needs to be done
        return 0;

    } else if(apr_fnmatch(TAR_GZ_PAT, info.path, 0) == APR_SUCCESS) {
        if(info.scheme) {
            rc = Shell_exec(CURL_SH,
                    "URL", url,
                    "TARGET", TAR_GZ_SRC, NULL);
            check(rc == 0, "Failed to curl source: %s", url);
        }

        rv = apr_dir_make_recursive(BUILD_DIR,
                APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);
        check(rv == APR_SUCCESS, "Failed to make directory %s", BUILD_DIR);

        rc = Shell_exec(TAR_SH, "FILE", TAR_GZ_SRC, NULL);
        check(rc == 0, "Failed to untar %s", TAR_GZ_SRC);
    } else if(apr_fnmatch(TAR_BZ2_PAT, info.path, 0) == APR_SUCCESS) {
        if(info.scheme) {
            rc = Shell_exec(CURL_SH, "URL", url, "TARGET", TAR_BZ2_SRC, NULL);
            check(rc == 0, "Curl failed.");
        }

        apr_status_t rc = apr_dir_make_recursive(BUILD_DIR,
                APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);

        check(rc == 0, "Failed to make directory %s", BUILD_DIR);
        rc = Shell_exec(TAR_SH, "FILE", TAR_BZ2_SRC, NULL);
        check(rc == 0, "Failed to untar %s", TAR_BZ2_SRC);
    } else {
        sentinel("Don't now how to handle %s", url);
    }

    // indicates that an install needs to actually run
    return 1;
error:
    return -1;
}

int Command_build(apr_pool_t *p, const char *url, const char *configure_opts,
        const char *make_opts, const char *install_opts)
{
    int rc = 0;

    check(access(BUILD_DIR, X_OK | R_OK | W_OK) == 0,
            "Build directory doesn't exist: %s", BUILD_DIR);

    // actually do an install
    if(access(CONFIG_SCRIPT, X_OK) == 0) {
        log_info("Has a configure script, running it.");
        rc = Shell_exec(CONFIGURE_SH, "OPTS", configure_opts, NULL);
        check(rc == 0, "Failed to configure.");
    }

    rc = Shell_exec(MAKE_SH, "OPTS", make_opts, NULL);
    check(rc == 0, "Failed to build.");

    rc = Shell_exec(INSTALL_SH,
            "TARGET", install_opts ? install_opts : "install",
            NULL);
    check(rc == 0, "Failed to install.");

    rc = Shell_exec(CLEANUP_SH, NULL);
    check(rc == 0, "Failed to cleanup after build.");

    rc = DB_update(url);
    check(rc == 0, "Failed to add this package to the database.");

    return 0;

error:
    return -1;
}

int Command_install(apr_pool_t *p, const char *url, const char *configure_opts,
        const char *make_opts, const char *install_opts)
{
    int rc = 0;
    check(Shell_exec(CLEANUP_SH, NULL) == 0, "Failed to cleanup before building.");

    rc = DB_find(url);
    check(rc != -1, "Error checking the install database.");

    if(rc == 1) {
        log_info("Package %s already installed.", url);
        return 0;
    }

    rc = Command_fetch(p, url, 0);

    if(rc == 1) {
        rc = Command_build(p, url, configure_opts, make_opts, install_opts);
        check(rc == 0, "Failed to build: %s", url);
    } else if(rc == 0) {
        // no install needed
        log_info("Depends successfully installed: %s", url);
    } else {
        // had an error
        sentinel("Install failed: %s", url);
    }

    Shell_exec(CLEANUP_SH, NULL);
    return 0;

error:
    Shell_exec(CLEANUP_SH, NULL);
    return -1;
}

在你輸入并編譯它之后,就可以開(kāi)始分析了貌笨。如果到目前為止你完成了前面的挑戰(zhàn)弱判,你會(huì)理解如何使用shell.c函數(shù)來(lái)運(yùn)行shell命令,以及參數(shù)如何被替換锥惋。如果沒(méi)有則需要回退到前面的挑戰(zhàn)昌腰,確保你真正理解了Shell_exec的工作原理。

挑戰(zhàn)3:評(píng)判我的設(shè)計(jì)

像之前一樣膀跌,完整地復(fù)查一遍代碼來(lái)保證一模一樣遭商。接著瀏覽每個(gè)函數(shù)并且確保你知道他如何工作。你也應(yīng)該跟蹤這個(gè)文件或其它文件中捅伤,每個(gè)函數(shù)對(duì)其它函數(shù)的調(diào)用劫流。最后,確認(rèn)你理解了這里的所有調(diào)用APR的函數(shù)丛忆。

一旦你正確編寫并分析了這個(gè)文件祠汇,把我當(dāng)成一個(gè)傻瓜一樣來(lái)評(píng)判我的設(shè)計(jì),我需要看看你是否可以改進(jìn)它熄诡。不要真正修改代碼可很,只是創(chuàng)建一個(gè)notes.txt并且寫下你的想法和你需要修改的地方。

devpkgmain函數(shù)

devpkg.c是最后且最重要的凰浮,但是也可能是最簡(jiǎn)單的文件我抠,其中創(chuàng)建了main函數(shù)。沒(méi)有與之配套的.h文件袜茧,因?yàn)檫@個(gè)文件包含其他所有文件菜拓。這個(gè)文件用于創(chuàng)建devpkg可執(zhí)行程序,同時(shí)組裝了來(lái)自Makefile的其它.o文件惫周。在文件中輸入代碼并保證正確尘惧。

#include <stdio.h>
#include <apr_general.h>
#include <apr_getopt.h>
#include <apr_strings.h>
#include <apr_lib.h>

#include "dbg.h"
#include "db.h"
#include "commands.h"

int main(int argc, const char const *argv[])
{
    apr_pool_t *p = NULL;
    apr_pool_initialize();
    apr_pool_create(&p, NULL);

    apr_getopt_t *opt;
    apr_status_t rv;

    char ch = '\0';
    const char *optarg = NULL;
    const char *config_opts = NULL;
    const char *install_opts = NULL;
    const char *make_opts = NULL;
    const char *url = NULL;
    enum CommandType request = COMMAND_NONE;


    rv = apr_getopt_init(&opt, p, argc, argv);

    while(apr_getopt(opt, "I:Lc:m:i:d:SF:B:", &ch, &optarg) == APR_SUCCESS) {
        switch (ch) {
            case 'I':
                request = COMMAND_INSTALL;
                url = optarg;
                break;

            case 'L':
                request = COMMAND_LIST;
                break;

            case 'c':
                config_opts = optarg;
                break;

            case 'm':
                make_opts = optarg;
                break;

            case 'i':
                install_opts = optarg;
                break;

            case 'S':
                request = COMMAND_INIT;
                break;

            case 'F':
                request = COMMAND_FETCH;
                url = optarg;
                break;

            case 'B':
                request = COMMAND_BUILD;
                url = optarg;
                break;
        }
    }

    switch(request) {
        case COMMAND_INSTALL:
            check(url, "You must at least give a URL.");
            Command_install(p, url, config_opts, make_opts, install_opts);
            break;

        case COMMAND_LIST:
            DB_list();
            break;

        case COMMAND_FETCH:
            check(url != NULL, "You must give a URL.");
            Command_fetch(p, url, 1);
            log_info("Downloaded to %s and in /tmp/", BUILD_DIR);
            break;

        case COMMAND_BUILD:
            check(url, "You must at least give a URL.");
            Command_build(p, url, config_opts, make_opts, install_opts);
            break;

        case COMMAND_INIT:
            rv = DB_init();
            check(rv == 0, "Failed to make the database.");
            break;

        default:
            sentinel("Invalid command given.");
    }


    return 0;

error:
    return 1;
}

為這個(gè)文件設(shè)置的挑戰(zhàn)是理解參數(shù)如何處理,以及參數(shù)是什么递递,之后創(chuàng)建含有使用指南的README文件喷橙。在編寫README的同時(shí)啥么,也編寫一個(gè)簡(jiǎn)單的simple.sh,它運(yùn)行./devpkg來(lái)檢查每個(gè)命令都在實(shí)際環(huán)境下工作贰逾。在你的腳本頂端使用set -e`悬荣,使它跳過(guò)第一個(gè)錯(cuò)誤。

最后疙剑,在Valgrind下運(yùn)行程序氯迂,確保在進(jìn)行下一步之前,所有東西都能正常運(yùn)行言缤。

期中檢測(cè)

最后的挑戰(zhàn)就是這個(gè)期中檢測(cè)嚼蚀,它包含三件事情:

  • 將你的代碼與我的在線代碼對(duì)比,以100%的分?jǐn)?shù)開(kāi)始管挟,每錯(cuò)一行減去1%轿曙。
  • 在你的notes.txt中記錄你是如何改進(jìn)代碼和devpkg的功能,并且實(shí)現(xiàn)你的改進(jìn)僻孝。
  • 編寫一個(gè)devpkg的替代版本导帝,使用其他你喜歡的語(yǔ)言,或者你覺(jué)得最適合編寫它的語(yǔ)言穿铆。對(duì)比二者您单,之后基于你的結(jié)果改進(jìn)你的devpkg的C版本。

你可以執(zhí)行下列命令來(lái)將你的代碼與我的對(duì)比:

cd ..  # get one directory above your current one
git clone git://gitorious.org/devpkg/devpkg.git devpkgzed
diff -r devpkg devpkgzed

這將會(huì)克隆我的devpkg版本到devpkgzed目錄中荞雏。之后使用工具diff來(lái)對(duì)比你的和我的代碼虐秦。書中你所使用的這些文件直接來(lái)自于這個(gè)項(xiàng)目,所以如果出現(xiàn)了不同的行讯檐,肯定就有錯(cuò)誤羡疗。

要記住這個(gè)練習(xí)沒(méi)有真正的及格或不及格染服,它只是一個(gè)方式來(lái)讓你挑戰(zhàn)自己别洪,并盡可能變得精確和謹(jǐn)慎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末柳刮,一起剝皮案震驚了整個(gè)濱河市挖垛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秉颗,老刑警劉巖痢毒,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蚕甥,居然都是意外死亡哪替,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門菇怀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)凭舶,“玉大人晌块,你說(shuō)我怎么就攤上這事∷” “怎么了匆背?”我有些...
    開(kāi)封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)身冀。 經(jīng)常有香客問(wèn)我钝尸,道長(zhǎng),這世上最難降的妖魔是什么搂根? 我笑而不...
    開(kāi)封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任珍促,我火速辦了婚禮,結(jié)果婚禮上剩愧,老公的妹妹穿的比我還像新娘踢星。我一直安慰自己,他們只是感情好隙咸,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布沐悦。 她就那樣靜靜地躺著,像睡著了一般五督。 火紅的嫁衣襯著肌膚如雪藏否。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天充包,我揣著相機(jī)與錄音副签,去河邊找鬼。 笑死基矮,一個(gè)胖子當(dāng)著我的面吹牛淆储,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播家浇,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼本砰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了钢悲?” 一聲冷哼從身側(cè)響起点额,我...
    開(kāi)封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎莺琳,沒(méi)想到半個(gè)月后还棱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惭等,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年珍手,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡琳要,死狀恐怖料扰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情焙蹭,我是刑警寧澤晒杈,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站孔厉,受9級(jí)特大地震影響拯钻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撰豺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一粪般、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧污桦,春花似錦亩歹、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至稼钩,卻和暖如春顾稀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背坝撑。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工静秆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人巡李。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓抚笔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親侨拦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子殊橙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容