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程序的程序;默认cc
CXX
: 编译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.