Shell编程与工程管理
Linux shell 编程
运维用到多一些,开发不是很常用,能看懂就行。
参考文献 :
《Linux shell编程 第三版》
《Linux shell核心编程指南》
https://www.runoob.com/linux/linux-shell-variable.html
基本知识
$0: 脚本名称;$1-$2->$...: 脚本的第一,二,…个参数;$*或者$@: 脚本的全部参数;- 相同点:都是引用所有参数。
- 不同点:只有在双引号中体现出来。假设在脚本运行时写了三个参数 1、2、3,则 “ * “ 等价于 “1 2 3”(传递了一个参数),而 “@” 等价于 “1” “2” “3”(传递了三个参数)。
$$: 脚本进程号;$#: 脚本参数个数;$?: 上一条指令返回结果;$-: 显示Shell使用的当前选项,与set命令功能相同。$!: 后台运行的最后一个进程的ID号。#!: 一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。- 函数与命令的执行结果可以作为条件语句使用。要注意的是,和 C 语言不同,shell 语言中 0 代表 true,0 以外的值代表 false。
变量定义
**字符串变量: ** 在 Shell中,变量通常被视为字符串。你可以使用单引号 ‘ 或双引号 “ 来定义字符串,例如:
1 | my_string='Hello, World!' |
整数变量: 在一些Shell中,你可以使用 declare 或 typeset 命令来声明整数变量。
1 | declare -i my_integer |
数组变量: Shell 也支持数组,允许你在一个变量中存储多个值。
1 | my_array=(1 2 3 4 5) |
关联数组:
1 | declare -A associative_array |
只读变量
1 | readonly Url="https://jelasin.github.io" |
删除变量
1 | unset variable_name |
功能语句
echo
Shell 的 echo 指令与 PHP 的 echo 指令类似,都是用于字符串的输出。命令格式:
1 | echo string |
您可以使用echo实现更复杂的输出格式控制。
显示普通字符串:
1 | echo "It is a test" |
这里的双引号完全可以省略,以下命令与上面实例效果一致:
1 | echo It is a test |
显示转义字符
1 | echo "\"It is a test\"" |
结果将是:
1 | "It is a test" |
同样,双引号也可以省略
显示变量
read 命令从标准输入中读取一行,并把输入行的每个字段的值指定给 shell 变量
1 | #!/bin/sh |
以上代码保存为 test.sh,name 接收标准输入的变量,结果将是:
1 | [root@www ~]# sh test.sh |
显示换行
1 | echo -e "OK! \n" # -e 开启转义 |
输出结果:
1 | OK! |
显示不换行
1 | #!/bin/sh |
输出结果:
1 | OK! It is a test |
显示结果定向至文件
1 | echo "It is a test" > myfile |
原样输出字符串,不进行转义或取变量(用单引号)
1 | echo '$name\"' |
输出结果:
1 | $name\" |
显示命令执行结果
1 | echo `date` |
注意: 这里使用的是反引号 `, 而不是单引号 ‘。
结果将显示当前日期
1 | Thu Jul 24 10:08:46 CST 2014 |
print/printf
printf 命令模仿 C 程序库(library)里的 printf() 程序。
printf 由 POSIX 标准所定义,因此使用 printf 的脚本比使用 echo 移植性好。
printf 使用引用文本或空格分隔的参数,外面可以在 printf 中使用格式化字符串,还可以制定字符串的宽度、左右对齐方式等。默认的 printf 不会像 echo 自动添加换行符,我们可以手动添加 \n。
printf 命令的语法:
1 | printf format-string [arguments...] |
参数说明:
- format-string: 一个格式字符串,它包含普通文本和格式说明符。
- arguments: 用于填充格式说明符的参数列表。。
格式说明符由 % 字符开始,后跟一个或多个字符,用于指定输出的格式。常用的格式说明符包括:
%s:字符串%d:十进制整数%f:浮点数%c:字符%x:十六进制数%o:八进制数%b:二进制数%e:科学计数法表示的浮点数
print和printf的异同点
- 自动换行与否:
- 输出格式:
printf允许进行格式化输出,可以控制输出的格式,例如指定字段宽度、精度、数据类型等。这使得printf更加灵活,适合于复杂的输出需求15。- 相比之下,
print则是按照默认分隔符打印变量或常量,输出相对简单,不支持格式化1。
- 使用场景:
- 通常,
printf更接近于 C 语言中的printf函数,适用于需要格式化的输出场景,能够提供更好的控制和清晰度5。 print更适合于简单的输出任务,不需要复杂的格式化处理时使用。
选择使用 print 还是 printf 主要取决于输出的复杂性和格式化需求。
read
在 Linux shell 编程中,read 命令是一个非常重要的内置命令,具有以下几个关键特点和用法:
功能:
read命令用于从标准输入(通常是键盘)读取一行数据,并可以将其赋值给一个或多个变量。这使得read特别适用于数据处理和交互式程序。
基本用法:
read可以通过该命令读取用户的输入,通常在脚本中用于获取用户输入的数值或文本。用户输入后,数据将被存储到指定的变量中。
读取文件的能力:
- 除了从标准输入读取数据外,
read还可以用来读取文件中的数据。通过重定向或管道,可以将文件的每一行传递给read命令。例如,可以使用cat命令并通过管道将文件内容传输给read。
- 除了从标准输入读取数据外,
语法选项:
read
命令有多种参数选项,包括:
-p:定义提示符,用于在读取之前显示给用户。-t:设置超时时间,若在指定时间内未输入则read会退出。-d:定义分隔符,以控制输入数据的结束。-s:静默读取。
退出状态:
- 当
read命令读取时,如果没有可读的行,它将以非零状态退出。这在脚本中可以通过检查退出状态来判断输入的有效性。
- 当
1 |
|
- 避免在脚本中直接读取密码:虽然
read命令的-s选项允许你静默地读取密码,但通常更好的做法是使用工具(如expect)来安全地处理密码。 - 确保输入验证:在使用
read命令读取用户输入时,始终确保对输入进行验证,以防止潜在的安全风险或脚本错误。 - 使用
-r选项来避免转义字符:如果你希望按原样读取输入(包括任何转义字符),请记得使用-r选项。 - 在脚本中提供清晰的提示:使用
-p选项为用户提供清晰的输入提示,以提高脚本的可用性。 - 处理超时和错误:当使用
-t选项设置超时时,确保你的脚本能够优雅地处理超时和错误情况。
test
在 Linux shell 编程中,test 是一个用于评估条件表达式的命令,常用于脚本中的条件判断。test 的功能非常强大,它可以用来比较数字、字符串、文件属性等。以下是对 test 的详细解释,包括它的用法和一些例子。
基本语法
test 的基本语法如下:
1 | test expression |
或者可以使用简洁的方括号表示法:
1 | [ expression ] |
在使用方括号时,必须在方括号和表达式之间留有空格。
数字比较
test 命令提供了一组用于数字比较的操作符:
| 操作符 | 描述 |
|---|---|
-eq |
等于 |
-ne |
不等于 |
-lt |
小于 |
-le |
小于或等于 |
-gt |
大于 |
-ge |
大于或等于 |
1 | a=5 |
或者
1 | if [ $a -lt $b ]; then |
字符串比较
test 命令也支持字符串比较:
| 操作符 | 描述 |
|---|---|
= |
等于 |
!= |
不等于 |
-z |
字符串长度为零 |
-n |
字符串长度非零 |
1 | str1="hello" |
文件测试
test 命令可以用来检查文件的属性,例如:
| 操作符 | 描述 |
|---|---|
-e |
文件存在 |
-f |
是否为常规文件 |
-d |
是否为目录 |
-r |
是否可读 |
-w |
是否可写 |
-x |
是否可执行 |
1 | file="/path/to/file" |
布尔逻辑
test 支持逻辑运算符,可以将条件组合在一起:
-a:与操作(逻辑与)-o:或操作(逻辑或)
在使用方括号时,可以使用双括号来更清晰地表示逻辑操作:
1 | if [ -f "$file" -a -r "$file" ]; then |
退出状态
test 的退出状态码很重要:
- 返回 0 表示条件为真。
- 返回非零状态码表示条件为假。
可以用 $? 来获取上一个命令的退出状态,例如:
1 | test -e "$file" |
expr
在 Linux shell 编程中,expr 是一个用于计算表达式的命令。它支持算术运算、逻辑运算和字符串操作。
基本功能
算术计算:
expr可以执行基本的算术运算,包括加法、减法、乘法、除法和取余。例如:1
2
3
4
5expr 5 + 3 # 结果是 8
expr 10 - 2 # 结果是 8
expr 4 \* 2 # 结果是 8(乘法需要使用反斜杠转义)
expr 9 / 3 # 结果是 3
expr 5 % 2 # 结果是 1
字符串操作
expr还可以用于字符串操作,如字符串长度和子串提取:- 长度: 获取字符串的长度
1
expr length "hello" # 结果是 5
- 子串提取: 提取字符串中的子串
1
expr substr "hello" 2 3 # 从第二个字符开始提取3个字符,结果是 "ell"
- 长度: 获取字符串的长度
比较操作
expr还可以做数值比较,返回 1(真)或 0(假):1
2
3
4expr 5 = 5 # 结果是 1
expr 5 != 3 # 结果是 1
expr 5 \> 3 # 结果是 1
expr 3 \< 5 # 结果是 1
逻辑运算
expr支持逻辑运算符,例如并且(&)和或者(|):1
2expr 1 \& 1 # 结果是 1
expr 1 \| 0 # 结果是 1
注意事项
- 在使用
expr时,建议使用反斜杠(\)对某些符号(如*、&、|)进行转义,因为它们在 shell 中有特殊含义。 - 在现代脚本中,许多功能可以用更强大的工具(如
bc、awk或者 bash 内置的算术扩展$(( ... )))来替代,expr的使用相对较少。
示例
一个简单的示例,计算两个数的和并输出:
1 | a=5 |
总之,expr 是一个功能强大的工具,适用于在 shell 脚本中进行简单的数学计算和字符串处理。然而,随着编程的复杂性增加,程序员往往会选择更现代及灵活的工具来完成复杂的任务。
sed
在 Linux shell 编程中,sed(stream editor)是一个非常强大的流编辑器,它用于对文本进行基本的转换和处理。它可以直接从标准输入读取数据,或者处理文件中的内容,支持正则表达式及各种替换、删除、插入等操作。以下是对 sed 的详解:
基本功能
文本替换:
sed最常用的功能是替换文本。基本语法为s/pattern/replacement/,表示将匹配pattern的部分替换为replacement。1
sed 's/old-text/new-text/' filename
该命令会将文件中的第一个
old-text替换为new-text。全局替换:
如果需要在一行中替换所有匹配的文本,可以使用g选项:1
sed 's/old-text/new-text/g' filename
删除操作
- 删除行:
使用d命令可以删除匹配特定模式的行:该命令会删除文件中所有匹配1
sed '/pattern/d' filename
pattern的行。
插入和追加文本
插入文本:
使用i命令可以在匹配行的前面插入文本:1
sed '/pattern/i text-to-insert' filename
追加文本:
使用a命令可以在匹配行的后面追加文本:1
sed '/pattern/a text-to-append' filename
选择行
- 打印特定行:
使用p命令可以打印匹配特定模式的行:1
sed -n '/pattern/p' filename
-n选项用于禁止默认输出,只有显式指定的行会被输出。
使用正则表达式
sed支持使用基本和扩展正则表达式进行模式匹配,这使得复杂的文本处理成为可能。
处理多个命令
- 可以使用
-e选项来指定多个sed命令:1
sed -e 's/old-text/new-text/' -e '/pattern/d' filename
备份与修改文件
- 使用
-i选项可以直接修改文件,并可选择创建备份:1
sed -i.bak 's/old-text/new-text/g' filename # 备份为 filename.bak
示例
一个简单示例,替换文件中的所有 hello 为 world,并打印输出:
1 | sed 's/hello/world/g' input.txt |
awk
在 Linux shell 编程中,awk 是一种强大的文本处理工具,用于扫描和处理文本文件,特别擅长处理结构化数据(如 CSV 或其他分隔符分隔的数据)。它的名称来源于其三位创始人的姓氏(Alfred Aho, Peter Weinberger 和 Brian Kernighan)。以下是 awk 的详解:
基本功能
字段分隔:
默认情况下,awk将每一行视为一个记录(record),并按空格或制表符分隔每个字段(field)。可以使用FS变量自定义字段分隔符,例如:1
awk -F ',' '{print $1}' filename # 使用逗号作为分隔符
打印字段:
awk使用$符号引用每一行的字段。$1表示第一字段,$2表示第二字段,以此类推。1
awk '{print $1, $2}' filename # 打印每行的第一个和第二个字段
条件处理
使用条件进行筛选:
可以在awk中使用条件语句进行行选择。例如,打印第二字段大于 100 的行:1
awk '$2 > 100' filename
结合打印与条件:
1
awk '$2 > 100 {print $1, $2}' filename # 只打印第二字段大于100的第一和第二字段
计算与聚合
算术运算:
awk允许进行算术运算:1
awk '{sum += $2} END {print sum}' filename # 计算第二字段的总和
平均值计算:
计算某字段的平均值:1
awk '{sum += $2; count++} END {print sum/count}' filename
模式与动作
awk中的基本语法是pattern { action }。当输入行匹配模式时,就执行相应的动作。
内置变量
awk提供了一些内置变量,如:NR:当前记录的行号。NF:当前记录的字段数量。$0:当前记录的整行内容。
例如:
1 | awk '{print NR, NF, $0}' filename # 打印行号、字段数和整行内容 |
使用函数
awk具有许多内置函数,如字符串处理、数学计算等。例如,使用length函数获取字符串长度:1
awk '{print length($1)}' filename # 打印每行第一个字段的长度
脚本方式
awk可以在命令行中使用,也可以写成脚本文件。例如:执行方式:1
2
3
4# myscript.awk
{
print $1, $2
}1
awk -f myscript.awk filename
示例
简单示例,计算文件中第二列的和:
1 | awk '{sum += $2} END {print sum}' filename |
I/O 重定向
大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回到您的终端。一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端。同样,一个命令通常将其输出写入到标准输出,默认情况下,这也是你的终端。
重定向命令列表如下:
| 命令 | 说明 |
|---|---|
| command > file | 将输出重定向到 file。 |
| command < file | 将输入重定向到 file。 |
| command >> file | 将输出以追加的方式重定向到 file。 |
| n > file | 将文件描述符为 n 的文件重定向到 file。 |
| n >> file | 将文件描述符为 n 的文件以追加的方式重定向到 file。 |
| n >& m | 将输出文件 m 和 n 合并。 |
| n <& m | 将输入文件 m 和 n 合并。 |
| << tag | 将开始标记 tag 和结束标记 tag 之间的内容作为输入。 |
需要注意的是文件描述符 0 通常是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。
控制语句
if
if 语句用于根据条件执行代码块。基本语法如下:
1 | if [ condition ]; then |
示例:
1 | if [ $age -ge 18 ]; then |
while
while 语句在条件为真时重复执行代码块。基本语法如下:
1 | while [ condition ]; do |
示例:
1 | count=1 |
for
for 语句用于遍历列表中的每个元素。基本语法如下:
1 | for variable in list; do |
示例:
1 | for fruit in apple banana cherry; do |
也可以使用 C 样式的 for 循环:
1 | for (( i=1; i<=5; i++ )); do |
case
case 语句用于根据变量的值匹配多个模式。基本语法如下:
1 | case variable in |
示例:
1 | case $day in |
until
until 语句与 while 相反,它在条件为假时重复执行代码块。基本语法如下:
1 | until [ condition ]; do |
示例:
1 | count=1 |
select
select 语句用于生成菜单,让用户选择。基本语法如下:
1 | select variable in list; do |
示例:
1 | select color in red green blue; do |
函数
在 Linux shell 编程中,函数是一种将一组命令封装在一起的机制,允许你在脚本中重复调用这些命令。使用函数可以使脚本更结构化、可读性更强,并且可以避免代码重复。以下是对 Shell 函数的详解,包括定义、调用、参数、返回值和作用域等内容。
定义函数
在 Bash 中,定义函数的基本语法如下:
1 | function_name () { |
或者使用 function 关键字:
1 | function function_name { |
函数示例
1 | greet() { |
函数参数
在函数内部,可以使用 $1、$2 等来访问传递给函数的参数。$1 代表第一个参数,$2 代表第二个参数,以此类推。$@ 和 $* 用于访问所有参数。
1 | add() { |
返回值
函数的返回值使用return命令,通常返回值是一个整数,范围从 0 到 255。也可以通过 echo 命令返回输出。
1 | multiply() { |
通过 echo 返回输出的示例:
1 | get_date() { |
本地变量与全局变量
- 全局变量:在函数外部定义的变量在函数内部也是可访问的。
- 局部变量:使用
local关键字定义的变量只在该函数内部可见。
1 | counter=0 |
函数的调用
函数可以在定义后随时调用。可以在脚本的任何位置调用已定义的函数。
1 | say_hello() { |
函数的作用域
函数的作用域通常存在以下几种情况:
- 在同一脚本中的任意位置都可以调用已经定义的函数。
- 如果在当前 shell 中定义的函数,也可以在子 shell 中调用,但局部变量不被共享。
递归函数
函数也可以调用自身,形成递归。这在某些算法(如计算阶乘)中非常有用。
1 | factorial() { |
函数的文档和帮助
为了使脚本更易于维护,可以在函数内部添加注释,或者使用 help 命令输出帮助信息。
1 | my_function() { |
shell 扩展
shell 中各种括号
单小括号 ()
- 命令组。括号中的命令将会新开一个子shell顺序执行,所以括号中的变量不能够被脚本余下的部分使用。括号中多个命令之间用分号隔开,最后一个命令可以没有分号,各命令和括号之间不必有空格。
- 命令替换。等同于
cmd,shell扫描一遍命令行,发现了$(cmd)结构,便将$(cmd)中的cmd执行一次,得到其标准输出,再将此输出放到原来命令。有些shell不支持,如tcsh。 - 用于初始化数组。如:array=(a b c d)
双小括号 (( ))
- 整数扩展。这种扩展计算是整数型的计算,不支持浮点型。((exp))结构扩展并计算一个算术表达式的值,如果表达式的结果为0,那么返回的退出状态码为1,或者 是”假”,而一个非零值的表达式所返回的退出状态码将为0,或者是”true”。若是逻辑判断,表达式exp为真则为1,假则为0。
- 只要括号中的运算符、表达式符合C语言运算规则,都可用在
$((exp))中,甚至是三目运算符。作不同进位(如二进制、八进制、十六进制)运算时,输出结果全都自动转化成了十进制。如:echo $((16#5f))结果为95 (16进位转十进制) - 单纯用
(( ))也可重定义变量值,比如a=5; ((a++))可将$a重定义为6 - 常用于算术运算比较,双括号中的变量可以不使用
$符号前缀。括号内支持多个表达式用逗号分开。 只要括号中的表达式符合C语言运算规则,比如可以直接使用for((i=0;i<5;i++)), 如果不使用双括号, 则为 for i in `seq 0 4` 或者for i in {0..4}。再如可以直接使用if (($i<5)), 如果不使用双括号, 则为if [ $i -lt 5 ]。
单中括号 []
- bash 的内部命令,[和test是等同的。如果我们不用绝对路径指明,通常我们用的都是bash自带的命令。if/test结构中的左中括号是调用test的命令标识,右中括号是关闭条件判断的。这个命令把它的参数作为比较表达式或者作为文件测试,并且根据比较的结果来返回一个退出状态码。if/test结构中并不是必须右中括号,但是新版的Bash中要求必须这样。
- Test和[]中可用的比较运算符只有==和!=,两者都是用于字符串比较的,不可用于整数比较,整数比较只能使用-eq,-gt这种形式。无论是字符串比较还是整数比较都不支持大于号小于号。如果实在想用,对于字符串比较可以使用转义形式,如果比较”ab”和”bc”:[ ab < bc ],结果为真,也就是返回状态为0。[ ]中的逻辑与和逻辑或使用-a 和-o 表示。
- 字符范围。用作正则表达式的一部分,描述一个匹配的字符范围。作为test用途的中括号内不能使用正则。
- 在一个array 结构的上下文中,中括号用来引用数组中每个元素的编号。
双中括号[[ ]]
- [[是 bash 程序语言的关键字。并不是一个命令,[[ ]] 结构比[ ]结构更加通用。在[[和]]之间所有的字符都不会发生文件名扩展或者单词分割,但是会发生参数扩展和命令替换。
- 支持字符串的模式匹配,使用=~操作符时甚至支持shell的正则表达式。字符串比较时可以把右边的作为一个模式,而不仅仅是一个字符串,比如[[ hello == hell? ]],结果为真。[[ ]] 中匹配字符串或通配符,不需要引号。
- 使用[[ … ]]条件判断结构,而不是[ … ],能够防止脚本中的许多逻辑错误。比如,&&、||、<和> 操作符能够正常存在于[[ ]]条件判断结构中,但是如果出现在[ ]结构中的话,会报错。比如可以直接使用
if [[ $a != 1 && $a != 2 ]], 如果不适用双括号, 则为if [ $a -ne 1] && [ $a != 2 ]或者if [ $a -ne 1 -a $a != 2 ]。 - bash 把双中括号中的表达式看作一个单独的元素,并返回一个退出状态码。。
文件包含
在 Linux shell 编程中,文件包含是一种将一个脚本文件的内容嵌入到另一个脚本中的方法。这有助于代码的重用和模块化。这样可以更容易地维护和组织代码,尤其是在处理大型脚本时。
包含文件的基本方法
在 Bash 中,有两种主要的方法来包含文件的内容:source 命令和点命令(.)。
source 命令
source 命令用于在当前 shell 环境中执行指定文件中的命令。其基本语法如下:
1 | source filename |
点命令
也可以使用一个点(.)来包含文件,效果等同于 source 命令。
1 | . filename |
示例
假设我们有一个包含函数的文件 my_functions.sh:
1 | # my_functions.sh |
我们可以在另一个脚本 main.sh 中包含这个文件,并使用这些函数:
1 | # main.sh |
当你运行 main.sh 时,输出将是:
1 | Hello, Alice! |
条件包含
有时,您可能希望仅在某个条件下包含某个文件。这可以使用 if 语句结合 [ -f filename ] 来实现,以确保文件存在。
1 | if [ -f "my_functions.sh" ]; then |
变量的作用域
通过 source 或点命令包含的文件中的变量将保留在当前的 shell 环境中。这意味着,您可以在包含的脚本中定义变量,并在主脚本中访问这些变量。
1 | # my_variables.sh |
1 | # main.sh |
错误处理
如果您尝试包含一个不存在的文件,source 命令将不会抛出错误,但上面的代码将继续执行。这可能会导致难以跟踪的错误。您可以在包含文件之前检查文件是否存在。
1 | if [ -f "my_functions.sh" ]; then |
项目构建工具
项目构建时很重要但也相对简单的一步,无论使用哪款构建工具,都能使用AGI都能很轻松的写出他们的构建脚本,但我们还是要学会他们的语言和优劣,以便在AGI出现幻觉时解决这些问题,学习一些知识时比如QT,也离不开这些工具。
Makefile
Makefile只适用于一些中小型项目,它的代码可读性非常差,前期学起来很麻烦。在构建速度上,比不上CMake这些现代构建工具(虽然CMake也要先生成Makefile, 但在生成前做了很多优化和检查,但CMake只适合C/C++),建议使用CMake或SCons这种现代构建工具,老项目,想继续维护的可以选择性的使用新构建工具。这里之所以讲Makefile完全是因为他是前朝遗老,很多经典,老的Unix/Linux项目使用它进行构建。
参考链接
https://liaoxuefeng.com/books/makefile/makefile-basic/index.html
http://makefiletutorial.foofun.cn/
https://www.gnu.org/software/make/
安装
1 | sudo apt install build-essential make gcc g++ clang |
在Linux环境下,当我们输入make命令时,它就在当前目录查找一个名为Makefile的文件,然后,根据这个文件定义的规则,自动化地执行任意命令,包括编译命令。
规则
Makefile由若干条规则(Rule)构成,每一条规则指出一个目标文件(Target),若干依赖文件(prerequisites),以及生成目标文件的命令。
Makefile由一组 rules 组成。 rule通常如下所示:
1 | targets: prerequisites |
- targets (目标) 是文件名,用空格分隔。 通常,每个rule只有一个。
- commands (命令) 是通常用于创建目标的一系列步骤。 这些 需要以制表符 开头,不可以是空格。
- prerequisites(先决条件) 也是文件名,用空格分隔。 在运行目标的命令之前,这些文件需要存在。 这些也称为 dependencies(依赖项)
例如,要生成m.txt,依赖a.txt与b.txt,规则如下:
我们举个例子:在当前目录下,有3个文本文件:a.txt,b.txt和c.txt。
现在,我们要合并a.txt与b.txt,生成中间文件m.txt,再用中间文件m.txt与c.txt合并,生成最终的目标文件x.txt,整个逻辑如下图所示:
1 | ┌─────┐ ┌─────┐ ┌─────┐ |
根据上述逻辑,我们来编写Makefile。
1 | # 目标文件: 依赖文件1 依赖文件2 |
注意:Makefile的规则中,命令必须以Tab开头,不能是空格。
一条规则的格式为目标文件: 依赖文件1 依赖文件2 ...,紧接着,以Tab开头的是命令,用来生成目标文件。上述第一条规则使用cat命令合并了a.txt与b.txt,并写入到m.txt,第二条规则。用什么方式生成目标文件make并不关心,因为命令完全是我们自己写的,可以是编译命令,也可以是cp、mv等任何命令。
以#开头的是注释,会被make命令忽略。
然后我们运行 make m.txt。 只要m.txt文件不存在,命令就会运行。 如果 m.txt 已存在,则不会运行任何命令。
需要注意的是,我所说的m.txt既是一个目标,也是一个文件。 那是因为两者直接绑在一起。 通常,当目标运行时(也就是运行目标的命令时),这些命令将创建一个与目标同名的文件(也就是 cat a.txt b.txt > m.txt)。
make使用文件的创建和修改时间来判断是否应该更新一个目标文件。
1 | x.txt: m.txt c.txt |
当我们执行make时,由于没有将目标作为参数提供给make命令,因此会运行第一个目标,也就是创建x.txt,但是由于x.txt依赖的文件m.txt不存在(另一个依赖c.txt已存在),故需要先执行规则m.txt创建出m.txt文件,再执行规则x.txt。执行完成后,当前目录下生成了两个文件m.txt和x.txt。
Makefile定义了一系列规则,每个规则在满足依赖文件的前提下执行命令,就能创建出一个目标文件。把默认执行的规则放第一条,其他规则的顺序是无关紧要的,因为make执行时自动判断依赖。此外,make会打印出执行的每一条命令,便于我们观察执行顺序以便调试。
伪目标
clean通常用作删除其他目标的输出的目标,但它在make中并不是一个专有的词。 你可以在此运行 make 和 make clean 来创建和删除 some_file。
请注意,clean在这里做了两件新的事情:
- 它不是第一个目标 (默认目标),也不是先决条件。 这意味着除非显式调用
make clean,否则它永远不会运行 - 它不是一个文件名。 如果你碰巧有一个名为
“clean”的文件,则此目标将无法运行,这不是我们想要的。
1 | some_file: |
将 .PHONY 添加到目标将防止Make将phony(假)目标与文件名混淆。 在此示例中,如果创建了文件 “clean”,则仍将运行 make clean。 从技术上讲,我应该在每个带有all或clean的示例中使用它。 此外,”phony” 目标通常具有很少是文件名的名称,实际上,许多人忽略了它。
1 | some_file: |
大型项目通常会提供clean、install这些约定俗成的伪目标名称,方便用户快速执行特定任务。
一般来说,并不需要用.PHONY标识clean等约定俗成的伪目标名称,除非有人故意搞破坏,手动创建名字叫clean的文件。
命令和执行
命令回显/静默
在命令之前添加 @ 以阻止其打印
你还可以运行带有-s的make命令,等同于在每行前面添加一个@
1 | all: |
命令执行
每个命令都在一个新的shell中运行(或者至少效果是这样的)
1 | all: |
默认Shell
默认Shell是 ‘/bin/sh’。你可以通过更改变量SHELL来更改此设置:
1 | SHELL=/bin/bash |
-k、-i、-错误处理
在运行时添加 -k,即使面对错误也要继续运行。 如果你想一次查看Make的所有错误,这将非常有用。
在命令前添加 - 以抑制错误
添加-i以使每个命令都会发生这种情况。
1 | one: |
中断或杀死Make
备注:如果你对make按下ctrl+c,它将删除它刚刚创建的较新的目标。
Make的递归使用
要递归调用makefile,请使用特殊的 $(MAKE) 而不是 make,因为这样它才能为你传递make标志,并且本身不会受到它们的影响。
1 | new_contents = "hello:\n\ttouch inside_file" |
使用export进行递归make
export指令采用一个变量,并使sub-make命令可以访问它。 在本例中,导出了cooly,以便subdir中的Makefile可以使用它。
注意: export具有与sh相同的语法,但它们不相关 (尽管在功能上相似)
1 | new_contents = "hello:\n\techo \$$(cooly)" |
你需要导出变量,以便让它们也在shell中运行。
1 | one=this will only work locally |
.EXPORT_ALL_VARIABLES 为你导出所有变量。
1 | .EXPORT_ALL_VARIABLES: |
make的参数
有一个很好的可以从Make运行的 options 列表。 查看 --dry-run,--touch,--old-file。
你可以有多个目标来make,例如 make clean run test,运行clean,然后run,然后test。
目标
all目标
要制作多个目标,而你想让所有目标都运行? 你可以制作一个all目标。 由于这是列出的第一个规则,因此如果调用 make 而未指定目标,则默认情况下它将运行。
1 | all: one two three |
多目标
当一个规则有多个目标时,将为每个目标运行命令。 $@ 是包含目标名称的自动变量。
1 | all: f1.o f2.o |
自动变量和通配符
* 通配符
* 和 % 在 Make中都被称为通配符,但它们的含义完全不同。 *会在你的文件系统中搜索匹配的文件名。 我建议你始终将其包装在wildcard 函数中,因为否则你可能会陷入下面描述的常见陷阱。
1 | # 打印出每个.c文件的文件信息 |
“*” 可以在目标,先决条件或 wildcard 函数中使用。
危险:*不能在变量定义中直接使用
危险: 当 * 不匹配任何文件时,它将维持原样 (除非在wildcard 函数中运行)
1 | thing_wrong := *.o # 不要这样做!‘*’不会展开 |
% 通配符
%确实很有用,但由于它可以在多种情况下使用,所以有点令人困惑。
- 在“匹配”模式下使用时,匹配字符串中的一个或多个字符。 这种匹配被称为词干(stem)。
- 在“替换”模式下使用时,它会获取匹配的词干,并替换字符串中的词干。
%最常用于规则定义和某些特定函数中。
自动变量
有很多个 自动变量 ,但通常只有几个出现:
1 | hey: one two |
其它规则
隐式规则
Make偏爱c编译。 而每次它出现这种偏爱的时候,事情就会变得混乱。 也许Make中最令人困惑的部分是Make的魔术/自动规则(magic/automatic rules)。 Make会调用这些“隐含的”规则。 我个人不同意这个设计决定,也不建议使用它们,但是它们经常被使用,因此很有用。 以下是隐含规则的列表:
- 编译C程序: 从
n.c自动生成n.o,命令形式为$(CC) -c $(CPPFLAGS) $(CFLAGS) - 编译C++程序:
n.o由n.cc或n.cpp自动生成,命令形式为$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) - 链接单个目标文件: 自动从
n.o构造n, 通过运行命令$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)
隐式规则使用的重要变量包括:
CC: 编译C程序的程序;默认ccCXX: 编译C++程序的程序; 默认g++CFLAGS: 要提供给C编译器的额外标志CXXFLAGS: 给C++编译器的额外标志CPPFLAGS: 要提供给C预处理器的额外标志LDFLAGS: 当编译器应该调用链接器时,会给他们额外的标志
让我们看看我们现在如何构建一个C程序,而无需明确地告诉制造如何进行编译:
1 | CC = gcc # 隐式规则标志 |
静态模式规则
静态模式规则是在Makefile中减少编写量的另一种方式,但我会说更有用,并且 “魔术” 技巧更少。 它们的语法如下:
1 | targets...: target-pattern: prereq-patterns ... |
本质是给定的 target 与 target-pattern 匹配 (通过 % 通配符)。 任何匹配的东西都被称为 词干。 然后将词干替换为 “prereq-pattern”,以生成目标的prereq。
一个典型的用例是将.c文件编译成.o文件。 这里是手动方式:
1 | objects = foo.o bar.o all.o |
这是使用静态模式规则的更高效的方式:
1 | objects = foo.o bar.o all.o |
静态模式规则和过滤器
当我稍后介绍函数时,我将预示你可以使用它们来做什么。 filter 过滤器函数可以在静态模式规则中使用,以匹配正确的文件。 在本例中,我编造了.raw和.result扩展名。
1 | obj_files = foo.result bar.o lose.o |
模式规则
模式规则经常被使用,但非常令人困惑。你可以从两个方面来看待它们:
- 定义自己的隐含规则的方法
- 静态模式规则的更简单形式
让我们首先从一个例子开始:
1 | # 定义将每个.c文件编译为.o文件的模式规则 |
模式规则在目标中包含%。 此 % 匹配任何非空字符串,其他字符匹配自己。 模式规则先决条件中的%代表与目标中的%匹配的同一词干。
这里是另一个例子:
1 | # 定义一个在先决条件中没有模式的模式规则。 |
双冒号规则
很少使用双冒号规则,但用它允许为同一目标定义多个规则。 如果这些规则是单冒号,则会打印一条警告,并且只运行这里的第二组命令。
1 | all: blah |
变量
类型和修饰
有两种类型的变量:
- 递归 (使用
=) - 在使用命令时才会查找变量,而不在定义命令时查找变量。 - 简单展开 (使用
:=) - 就像普通的命令式编程一样 – 只有到目前为止定义的变量才会得到展开
1 | # 递归变量。这将能在下面打印出 “later” |
简单的说展开(使用:=)允许你追加到一个变量。 递归定义可能会给出无限循环错误。
1 | one = hello |
?= 仅设置尚未设置的变量
1 | one = hello |
行尾的空格不会被去掉,但行首的空格会被去掉。 要使用单个空格制作变量,请使用 $(nullstring)
1 | with_spaces = hello # with_spaces在 "hello" 之后有很多空格 |
未定义的变量实际上是空字符串!
1 | all: |
使用 += 追加
1 | foo := start |
字符串替换也是一种真正常见且有用的修改变量的方法。 另请查看 Text Functions 和 Filename Functions 。
命令行参数和覆盖
你可以使用override覆盖来自命令行的变量。 在这里,我们用 make option_one=hi 运行 make
1 | # 覆盖命令行参数 |
命令列表和define
“define”实际上只是一个命令列表。 它与函数无关。 请注意,这与命令之间的分号有点不同,因为每个命令都在单独的shell中运行,如预期的那样。
1 | one = export blah="I was set!"; echo $$blah |
目标特定变量
可以为特定目标中赋值变量
1 | all: one = cool |
模式特定变量
你可以为特定目标模式中分配变量
1 | %.c: one = cool |
条件
条件if/else
1 | foo = ok |
检查变量是否为空
1 | nullstring = |
检查是否定义了变量
ifdef不展开变量引用;它只查看是否定义了某些内容
1 | bar = |
$(makeflags)
此示例向你展示了如何使用 findstring 和 MAKEFLAGS 测试make标志。 使用make -i运行此示例,以查看它打印出echo语句。
1 | bar = |
函数
首个函数
函数主要用于文本处理。 用 $(fn, arguments) 或 ${fn, arguments} 调用函数。 你可以使用 call 内置函数来创建自己的代码。 Make有相当数量的 内置函数 。
1 | bar := ${subst not, totally, "I am not superman"} |
如果要使用变量替换空格或逗号
1 | comma := , |
第一个参数后不要包含空格。 否则这将被视为字符串的一部分。
1 | comma := , |
字符串替换
$(patsubst pattern,replacement,text)执行以下操作:
“在与pattern匹配的text中查找空格分隔的单词,并将其替换为replacement。 在这里,模式可以包含充当通配符的‘%’,匹配单词中任意数量的任何字符。 如果替换还包含 ‘%’,则将 ‘%’ 替换为与模式中的 ‘%’ 匹配的文本。 只有模式和替换中的第一个‘%’会以这种方式处理;任何后续的‘%’都不会改变。” (GNU docs)
替换引用 $(text:pattern=replacement) 是这方面的简写。
还有一个只替换后缀的简写:$(text:suffix=replacement)。 这里不使用 % 通配符。
注意:不要为此简写添加额外的空格。 它将被视为搜索或替换术语。
1 | foo := a.o b.o l.a c.o |
foreach函数
foreach函数如下所示: $(foreach var,list,text)。 它将一个单词列表(由空格分隔)转换为另一个列表。 var被设置为list中每个单词,同时text是针对每一个单词的展开。
这里在每个单词后附加了一个感叹号:
1 | foo := who are you |
if函数
if检查第一个参数是否为非空。如果是,则运行第二个参数,否则运行第三个参数。
1 | foo := $(if this-is-not-empty,then!,else!) |
调用函数
Make支持创建基本函数。 你只需创建一个变量即可定义函数,但需要使用$(0)、$(1)等参数。 然后,你使用特殊的 ‘call’ 函数调用该函数。 语法为$(call variable,param,param)。 $(0) 是变量,而 $(1),$(2)… 等是参数。
1 | sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3) |
shell函数
shell - 这调用shell,但它用空格替换换行符!
1 | all: |
其他特性
包含Makefiles
include指令告诉make读取一个或多个其他多个makefiles。 一行makefile中makefile,看起来像这样:
1 | include filenames... |
当你使用诸如 -M 之类的编译器标志基于源创建Makefiles时,这特别有用。 例如,如果某些c文件包含头文件,则该头文件将被添加到由GCC编写的Makefile中。 我在 Makefile Cookbook 中有更多探讨
vpath指令
使用vpath指定一些先决条件的存在位置。 格式为vpath <pattern> <directories, space/colon separated><pattern> 可以有一个 %,它匹配任何零个或多个字符。
你还可以使用变量VPATH在全局范围内执行此操作
1 | vpath %.h ../headers ../other-directory |
多行
反斜杠(“")字符使我们能够在命令太长时使用多行
1 | some_file: |
.phony
将 .PHONY 添加到目标将防止Make将phony(假)目标与文件名混淆。 在此示例中,如果创建了文件 “clean”,则仍将运行 make clean。 从技术上讲,我应该在每个带有all或clean的示例中使用它,但我没有保持clean示例。 此外,”phony” 目标通常具有很少是文件名的名称,实际上,许多人忽略了它。
1 | some_file: |
.delete_on_error
如果命令返回非零退出状态,则make工具将停止运行规则(并将传播回先决条件)。
如果规则以这种方式失败,则 DELETE_ON_ERROR 将删除规则的目标。 这将发生在所有目标上,而不仅仅是它之前的那个PHONY目标。 最好始终使用它是一个好主意,即使make出于历史原因没有默认使用这个策略。
1 | .DELETE_ON_ERROR: |
示例
1 | # 感谢Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/) |
多文件目录管理
在主Makefile中,我们可以这样定义一个递归构建规则:
1 | all: |
在上述示例中,$(MAKE) -C src表示调用src目录下的Makefile构建目标;$(MAKE) -C lib表示调用lib目录下的Makefile构建目标。
自动查找子目录
我们可以通过wildcard和foreach来自动查找所有子目录,并自动生成子目录规则。示例如下:
1 | SUBDIRS := $(wildcard */) |
在上述示例中,wildcard */会查找所有的子目录;patsubst %/, %/Makefile, $(SUBDIRS)会自动生成所有子目录对应的Makefile路径。
export
我们可以通过 export 导出变量供子目录使用。
1 | CC := gcc |
include
在Makefile使用include关键字可以把别的Makefile包含进来,这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。 比如命令
1 | include file.dep |
即把file.dep文件在当前Makefile文件中展开,亦即把file.dep文件的内容包含进当前Makefile文件
在 include前面可以有一些空字符,但是绝不能是[Tab]键开始。
常用模板
主Makefile
1 | CC := gcc |
fun、global、main文件夹使用同样的Makefile文件
1 | #获取当前目录先的.c文件信息 |
obj目录
1 | $(BIN_DIR)/$(TARGET) : $(wildcard *.o) |
Kbuild/Kconfig
CMake
虽然CMake是基于Makefile的,但是相较于 Makefile,CMake更适用于大型项目,并且更易于维护,构建速度,可读性也比Makefile好很多(虽然CMake也要先生成Makefile, 但在生成前做了很多优化和检查)。Makefile适用于一些中小型项目。
在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:
- 写
CMake配置文件CMakeLists.txt。 - 执行命令
cmake PATH或者ccmake
PATH 生成 Makefile(ccmake 和 cmake 的区别在于前者提供了一个交互式的界面)。其中, PATH 是 CMakeLists.txt 所在的目录。
也可以使用 make 命令进行编译。
注意:虽然 CMake 支持大写、小写和混合大小写的命令,但首选小写命令,本教程将始终使用小写命令。
参考链接
https://cmake.com.cn/cmake/help/latest/guide/tutorial/A%20Basic%20Starting%20Point.html
单个源文件
最基本的 CMake 项目是从单个源代码文件构建的可执行文件。对于这样的简单项目,仅需要一个包含三个命令的 CMakeLists.txt 文件。
1 | cmake_minimum_required(VERSION 3.10) |
任何项目的顶级 CMakeLists.txt 必须首先使用 cmake_minimum_required() 命令指定 CMake 的最低版本。这将建立策略设置并确保以下 CMake 函数与兼容版本的 CMake 一起运行。
要启动项目,我们使用 project() 命令设置项目名称。此调用对于每个项目都是必需的,并且应在 cmake_minimum_required() 之后立即调用。正如我们稍后将看到的那样,此命令还可用于指定其他项目级信息,例如语言或版本号。
最后,add_executable() 命令告诉 CMake 使用指定的源代码文件创建可执行文件。
构建,编译和运行
推荐使用外部构建,把生成文件和项目文件分开来构建,内部构建会破坏原有代码结构。
1 | # ----------------------------------------------- |
Windows 下,CMake 默认使用微软的 MSVC 作为编译器,想使用 MinGW 编译器,可以通过
-G参数来进行指定,只有第一次构建项目时需要指定。
1 cmake -G "MinGW Makefiles" ..
优化 CMakeLists.txt 文件
set 与 PROJECT_NAME
1 | cmake_minimum_required(VERSION 3.15) |
指定了项目名后,后面可能会有多个地方用到这个项目名,如果更改了这个名字,就要改多个地方,比较麻烦,那么可以使用 PROJECT_NAME 来表示项目名。
1 | add_executable(${PROJECT_NAME} tutorial.cpp) |
生成可执行文件需要指定相关的源文件,如果有多个,那么就用空格隔开,比如:
1 | add_executable(${PROJECT_NAME} a.cpp b.cpp c.cpp) |
我们也可以用一个变量来表示这多个源文件:
1 | set(SRC_LIST a.cpp b.cpp c.cpp) |
set 命令指定 SRC_LIST 变量来表示多个源文件,用 ${var_name} 获取变量的值。
添加版本号和配置头文件
1 | cmake_minimum_required(VERSION 3.15) |
由于 TutorialConfig.h 文件这里被设置为自动写入 build 目录,因此需要将该目录添加到搜索头文件的路径列表中,也可以修改为写到其它目录。
PROJECT_BINARY_DIR 表示当前工程的二进制路径,即编译产物会存放到该路径,此时PROJECT_BINARY_DIR 就是 build 所在路径。
手动创建 TutorialConfig.h.in 文件,包含以下内容:
1 | // the configured options and settings for Tutorial |
当使用 CMake 构建项目后,会在 build 中生成一个 TutorialConfig.h 文件,内容如下:
1 | // the configured options and settings for Tutorial |
下一步在 tutorial.cpp 包含头文件 TutorialConfig.h,最后通过以下代码打印出可执行文件的名称和版本号。
1 | if (argc < 2) { |
添加编译时间戳
有时候我们需要知道编译时的时间戳,并在程序运行时打印出来。那就需要在 CMakeLists.txt 中添加如下这句:
1 | string(TIMESTAMP COMPILE_TIME %Y%m%d-%H%M%S) |
这表示将时间戳已指定格式保存到 COMPILE_TIME 变量中。
然后修改上面的 TutorialConfig.h.in 文件:
1 | // the configured options and settings for Tutorial |
在构建项目后,TutorialConfig.h 文件就会自动增加一句:
1 |
这样就可以在源码中打印出 TIMESTAMP 的值了。
也可以直接通过
__TIMESTAMP__变量打印时间戳。
指定 C/C++ 标准
我们可以在CMake代码中设置正确的标志,以启动对特定C/C++标准的支持。最简单的是使用CMAKE_CXX_STANDARD变量。
注意:要在add_executable之前,添加对CMAKE_CXX_STANDARD的声明。
脚本中set是将普通变量、缓存变量或者环境变量设置为指定的值。
1 | #启动对C++14标准的支持 |
添加库
现在我们将向项目中添加一个库,这个库包含计算数字平方根的实现,可执行文件使用这个库,而不是编译器提供的标准平方根函数。
我们把库放在名为 MathFunctions 的子目录中。此目录包含头文件 MathFunctions.h 和源文件 mysqrt.cpp。源文件有一个名为 mysqrt 的函数,它提供了与编译器的 sqrt 函数类似的功能,MathFunctions.h 则是该函数的声明。
在 MathFunctions 目录下创建一个 CMakeLists.txt 文件,并添加以下一行:
1 | # MathFunctions/CMakeLists.txt |
表示添加一个叫 MathFunctions 的库文件。
CMake 中的 target 有可执行文件和库文件,分别使用 add_executable 和 add_library 命令生成,除了指定生成的可执行文件名/库文件名,还需要指定相关的源文件。
此时文件结构为:
1 | Demo/ |
为了使用 MathFunctions 这个库,我们将在顶级 CMakeLists.txt 文件中添加一个 add_subdirectory(MathFunctions) 命令指定库所在子目录,该子目录下应包含 CMakeLists.txt 文件和代码文件。
可执行文件要使用库文件,需要能够找到库文件和对应的头文件,可以分别通过 target_link_libraries 和 target_include_directories 来指定。
使用 target_link_libraries 将新的库文件添加到可执行文件中,使用 target_include_directories 将 MathFunctions 添加为头文件目录,添加到 Tutorial 目标上,以便 mysqrt.h 可以被找到。
顶级 CMakeLists.txt 的最后几行如下所示:
1 | # add the MathFunctions library |
MathFunctions 库就算添加完成了,接下来就是在主函数使用该库中的函数,先在 tutorial.cpp 文件中添加头文件:
1 |
然后使用 mysqrt 函数即可:
1 | const double outputValue = mysqrt(inputValue); |
将库设置为可选项
现在将 MathFunctions 库设为可选的,虽然对于本教程来说,没有必要这样做,但对于较大的项目来说,这种情况很常见。
第一步是向顶级 CMakeLists.txt 文件添加一个选项。
1 | option(USE_MYMATH "Use tutorial provided math implementation" ON) |
option 表示提供用户可以选择的选项。命令格式为:option(<variable> "description [initial value])。
USE_MYMATH 这个选项缺省值为 ON,用户可以更改这个值。此设置将存储在缓存中,以便用户不需要在每次构建项目时设置该值。
下一个更改是使 MathFunctions 库的构建和链接成为条件。于是创建一个 if 语句,该语句检查选项 USE_MYMATH 的值。
1 | if(USE_MYMATH) |
在 if 块中,有 add_subdirectory 命令和 list 命令,APPEND表示将元素MathFunctions追加到列表EXTRA_LIBS中,将元素 ${PROJECT_SOURCE_DIR}/MathFunctions 追加到列表EXTRA_INCLUDES中。EXTRA_LIBS 存储 MathFunctions 库,EXTRA_INCLUDES 存储 MathFunctions 头文件。
变量EXTRA_LIBS用来保存需要链接到可执行程序的可选库,变量EXTRA_INCLUDES用来保存可选的头文件搜索路径。这是处理可选组件的经典方法,我将在下一步介绍现代方法。
接下来对源代码的进行修改。首先,在 tutorial.cpp 中包含 MathFunctions.h 头文件:
1 | #ifdef USE_MYMATH |
然后,还在 tutorial.cpp 中,使用 USE_MYMATH 选择使用哪个平方根函数:
1 |
|
因为源代码使用了 USE_MYMATH 宏,可以用下面的行添加到 tutorialconfig.h.in 文档中:
1 | // TutorialConfig.h.in |
现在使用 cmake 命令构建项目,并运行生成的 Tutorial 可执行文件。
1 | build> cmake -G"MinGW Makefiles" .. |
默认调用 mysqrt 函数,也可以在构建项目时指定 USE_MYMATH 的值为 OFF:
1 | > cmake -DUSE_MYMATH=OFF .. |
此时会调用自带的 sqrt 函数。
添加库的使用要求
使用要求会对库或可执行程序的链接、头文件包含命令行提供了更好的控制,也使 CMake 中目标的传递目标属性更加可控。利用使用要求的主要命令是:
target_compile_definitions()target_compile_options()target_include_directories()target_link_libraries()
现在重构一下之前中的代码,使用更加现代的 CMake 方法来包含 MathFunctions 库的头文件。
首先声明,链接 MathFunctions 库的任何可执行文件/库文件都需要包含 MathFunctions 目录作为头文件路径,而 MathFunctions 本身不需要包含,这被称为 INTERFACE 使用要求。
INTERFACE是指消费者需要、但生产者不需要的那些东西。在MathFunctions/CMakeLists.txt 最后添加:
1 | # MathFunctions/CMakeLists.txt |
CMAKE_CURRENT_SOURCE_DIR 表示 MathFunctions 库所在目录。
现在我们已经为 MathFunctions 指定了使用要求 INTERFACE,那么可以从顶级 CMakeLists.txt 中删除EXTRA_INCLUDES变量的相关使用:
1 | if(USE_MYMATH) |
现在只要是链接了 MathFunctions 库,就会自动包含 MathFunctions 所在目录的头文件,简洁而优雅。
这里补充两个知识点:
1、使用要求除了 INTERFACE,还有PRIVATE 和 PUBLIC。INTERFACE表示消费者需要生产者不需要,PRIVATE表示消费者不需要生产者需要,PUBLIC 表示消费者和生产者都需要。
2、这里使用 add_library 命令生成的 MathFunctions 库其实是静态链接库。
build 目录介绍
在文本中,我都是创建了一个 build 用来存放 cmake 构建和编译的产物,这里简单说下里面有些什么东西。
1 | build/ |
其中 Makefile 是 cmake 根据顶级 CMakeLists.txt 生成的构建文件,通过该文件可以对整个项目进行编译。
Tutorial.exe 就是生成的可执行文件,通过该文件运行程序。
TutorialConfig.h 是用于配置信息的头文件,是 cmake 根据 TutorialConfig.h.in 文件自动生成的。
还有个 MathFunctions 文件夹:
1 | MathFunctions/ |
其中 Makefile 是 cmake 根据 MathFunctions 目录下的 CMakeLists.txt 生成的构建文件。
libMathFunctions.a 则是 MathFunctions 静态链接库,可执行文件会通过这个库调用 mysqrt 函数。
安装和测试
CMake 也可以指定安装规则,以及添加测试。这两个功能分别可以通过在产生 Makefile 后使用 make install 和 make test 来执行。在以前的 GNU Makefile 里,你可能需要为此编写 install 和 test 两个伪目标和相应的规则,但在 CMake 里,这样的工作同样只需要简单的调用几条命令。
定制安装规则
首先先在 math/CMakeLists.txt 文件里添加下面两行:
1 | # 指定 MathFunctions 库的安装路径 |
指明 MathFunctions 库的安装路径。之后同样修改根目录的 CMakeLists 文件,在末尾添加下面几行:
1 | # 指定安装路径 |
通过上面的定制,生成的 Demo 文件和 MathFunctions 函数库 libMathFunctions.o 文件将会被复制到 /usr/local/bin 中,而 MathFunctions.h 和生成的 config.h 文件则会被复制到 /usr/local/include 中。我们可以验证一下(顺带一提的是,这里的 /usr/local/ 是默认安装到的根目录,可以通过修改 CMAKE_INSTALL_PREFIX 变量的值来指定这些文件应该拷贝到哪个根目录):
1 | [ehome@xman Demo5]$ sudo make install |
为工程添加测试
添加测试同样很简单。CMake 提供了一个称为 CTest 的测试工具。我们要做的只是在项目根目录的 CMakeLists 文件中调用一系列的 add_test 命令。
1 | # 启用测试 |
上面的代码包含了四个测试。第一个测试 test_run 用来测试程序是否成功运行并返回 0 值。剩下的三个测试分别用来测试 5 的 平方、10 的 5 次方、2 的 10 次方是否都能得到正确的结果。其中 PASS_REGULAR_EXPRESSION 用来测试输出是否包含后面跟着的字符串。让我们看看测试的结果:
1 | [ehome@xman Demo5]$ make test |
如果要测试更多的输入数据,像上面那样一个个写测试用例未免太繁琐。这时可以通过编写宏来实现:
1 | # 定义一个宏,用来简化测试工作 |
关于 CTest 的更详细的用法可以通过 man 1 ctest 参考 CTest 的文档。
生成安装包
本节将学习如何配置生成各种平台上的安装包,包括二进制安装包和源码安装包。为了完成这个任务,我们需要用到 CPack ,它同样也是由 CMake 提供的一个工具,专门用于打包。首先在顶层的 CMakeLists.txt 文件尾部添加下面几行:
1 | # 构建一个 CPack 安装包 |
上面的代码做了以下几个工作:
- 导入 InstallRequiredSystemLibraries 模块,以便之后导入 CPack 模块;
- 设置一些 CPack 相关变量,包括版权信息和版本信息,其中版本信息用了上一节定义的版本号;
- 导入 CPack 模块。
接下来的工作是像往常一样构建工程,并执行 cpack 命令。
- 生成二进制安装包:
1 | cpack -C CPackConfig.cmake |
- 生成源码安装包
1 | cpack -C CPackSourceConfig.cmake |
我们可以试一下。在生成项目后,执行 cpack -C CPackConfig.cmake 命令:
1 | [ehome@xman Demo8]$ cpack -C CPackSourceConfig.cmake |
此时会在该目录下创建 3 个不同格式的二进制包文件:
1 | [ehome@xman Demo8]$ ls Demo8-* |
这 3 个二进制包文件所包含的内容是完全相同的。我们可以执行其中一个。此时会出现一个由 CPack 自动生成的交互式安装界面:
1 | [ehome@xman Demo8]$ sh Demo8-1.0.1-Linux.sh |
完成后提示安装到了 Demo8-1.0.1-Linux 子目录中,我们可以进去执行该程序:
1 | [ehome@xman Demo8]$ ./Demo8-1.0.1-Linux/bin/Demo 5 2 |
关于 CPack 的更详细的用法可以通过 man 1 cpack 参考 CPack 的文档。
版本控制工具
Git
Git是一个分布式版本控制系统,广泛用于软件开发中以跟踪和管理代码的变更。以下是一个基本的Git教程,帮助你入门Git的使用。
安装Git
- Windows: 可以从Git官网下载Git Bash。
- macOS: 使用Homebrew安装:
brew install git - Linux: 使用包管理器安装,例如在Ubuntu上:
sudo apt-get install git
基本配置
安装完成后,进行一些基本配置:
1 | git config --global user.name "Your Name" |
创建仓库
初始化一个新的Git仓库:
1
git init
克隆一个现有的仓库:
1
git clone <repository-url>
基本操作
查看状态:
1
git status
添加文件到暂存区:
1
git add <file1> <file2>
或者添加所有更改:
1
git add .
提交更改:
1
git commit -m "Commit message"
查看提交历史:
1
git log
分支管理
查看分支:
1
git branch
创建新分支:
1
git branch <new-branch-name>
切换分支:
1
git checkout <branch-name>
创建并切换到新分支:
1
git checkout -b <new-branch-name>
合并分支:
切换到目标分支(如
main),然后合并:1
2git checkout main
git merge <branch-name>
远程操作
查看远程仓库:
1
git remote -v
添加远程仓库:
1
git remote add origin <repository-url>
推送到远程仓库:
1
git push origin <branch-name>
拉取远程更改:
1
git pull origin <branch-name>
撤销更改
撤销工作区中的更改:
1
git checkout -- <file>
撤销已暂存的更改:
1
git reset HEAD <file>
回滚到之前的提交:
1
git reset --hard <commit-id>
高级功能
- 分支管理和协作: 了解如何在团队中使用分支进行协作。
- Git标签: 用于标记重要的提交,如发布版本。
- Git Rebase: 用于整理提交历史,保持提交历史的整洁。
学习资源
- Pro Git: 一本免费的电子书,详细介绍了Git的所有功能。
- Git官方文档: 提供了Git命令的详细解释和使用示例。
- 在线教程: 如GitHub提供的Git学习路径。
通过学习和实践这些基本操作,你将能够有效地使用Git来管理你的项目代码。随着经验的积累,可以探索更多高级功能,如Git Hooks、Submodules、Rebasing等。
Repo
- Title: Shell编程与工程管理
- Author: 韩乔落
- Created at : 2025-02-01 15:11:31
- Updated at : 2025-03-27 16:53:44
- Link: https://jelasin.github.io/2025/02/01/Shell编程与工程管理/
- License: This work is licensed under CC BY-NC-SA 4.0.