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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
my_string='Hello, World!'
my_string="Hello, World!"

# 获取字符串长度
# 变量为字符串时,${#string} 等价于 ${#string[0]}:
string="abcd"
echo ${#string} # 输出 4
echo ${#string[0]} # 输出 4

# 提取子字符串
# 实例从字符串第 2 个字符开始截取 4 个字符:
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo

# 查找子字符串
# 查找字符 i 或 o 的位置(哪个字母先出现就计算哪个):
string="runoob is a great site"
echo `expr index "$string" io` # 输出 4

整数变量: 在一些Shell中,你可以使用 declaretypeset 命令来声明整数变量。

1
declare -i my_integer

数组变量: Shell 也支持数组,允许你在一个变量中存储多个值。

1
2
3
4
5
6
7
8
my_array=(1 2 3 4 5)

# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
length=${#array_name[n]}

关联数组:

1
2
3
declare -A associative_array
associative_array["name"]="John"
associative_array["age"]=30

只读变量

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
2
3
#!/bin/sh
read name
echo "$name It is a test"

以上代码保存为 test.sh,name 接收标准输入的变量,结果将是:

1
2
3
[root@www ~]# sh test.sh
OK #标准输入
OK It is a test #输出

显示换行

1
2
echo -e "OK! \n" # -e 开启转义
echo "It is a test"

输出结果:

1
2
3
OK!

It is a test

显示不换行

1
2
3
4
5
#!/bin/sh
echo -e "OK! \c" # -e 开启转义 \c 不换行
echo "It is a test"
# 或者
echo -n "It is a test"

输出结果:

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

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的异同点

  1. 自动换行与否:
  • print 命令会在输出内容后自动换行,即每打印一个输出项,行尾都会添加换行符12
  • printf 命令,则不会自动换行,用户需要在格式字符串中手动指定换行符(例如使用 \n5
  1. 输出格式:
  • printf 允许进行格式化输出,可以控制输出的格式,例如指定字段宽度、精度、数据类型等。这使得 printf 更加灵活,适合于复杂的输出需求15
  • 相比之下,print 则是按照默认分隔符打印变量或常量,输出相对简单,不支持格式化1
  1. 使用场景:
  • 通常,printf 更接近于 C 语言中的 printf 函数,适用于需要格式化的输出场景,能够提供更好的控制和清晰度5
  • print 更适合于简单的输出任务,不需要复杂的格式化处理时使用。

选择使用 print 还是 printf 主要取决于输出的复杂性和格式化需求。

read

在 Linux shell 编程中,read 命令是一个非常重要的内置命令,具有以下几个关键特点和用法:

  1. 功能:

    • read 命令用于从标准输入(通常是键盘)读取一行数据,并可以将其赋值给一个或多个变量。这使得 read 特别适用于数据处理和交互式程序。
  2. 基本用法:

    • read 可以通过该命令读取用户的输入,通常在脚本中用于获取用户输入的数值或文本。用户输入后,数据将被存储到指定的变量中。
  3. 读取文件的能力:

    • 除了从标准输入读取数据外,read 还可以用来读取文件中的数据。通过重定向或管道,可以将文件的每一行传递给 read 命令。例如,可以使用 cat 命令并通过管道将文件内容传输给 read
  4. 语法选项:

    • read

      命令有多种参数选项,包括:

      • -p:定义提示符,用于在读取之前显示给用户。
      • -t:设置超时时间,若在指定时间内未输入则 read 会退出。
      • -d:定义分隔符,以控制输入数据的结束。
      • -s:静默读取。
  5. 退出状态:

    • read 命令读取时,如果没有可读的行,它将以非零状态退出。这在脚本中可以通过检查退出状态来判断输入的有效性。
1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
if read -t 5 -p "请在5秒内输入一些内容: " input; then
echo "你输入了: $input"
else
echo "超时了,你没有输入任何内容"
fi

read -p "请输入你的密码: " -s password
echo
echo "密码已读取(但不会显示)"

  • 避免在脚本中直接读取密码:虽然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
2
3
4
5
6
a=5
b=10

if test $a -lt $b; then
echo "$a is less than $b"
fi

或者

1
2
3
if [ $a -lt $b ]; then
echo "$a is less than $b"
fi

字符串比较

test 命令也支持字符串比较:

操作符 描述
= 等于
!= 不等于
-z 字符串长度为零
-n 字符串长度非零
1
2
3
4
5
6
str1="hello"
str2="world"

if [ "$str1" != "$str2" ]; then
echo "$str1 is not equal to $str2"
fi

文件测试

test 命令可以用来检查文件的属性,例如:

操作符 描述
-e 文件存在
-f 是否为常规文件
-d 是否为目录
-r 是否可读
-w 是否可写
-x 是否可执行
1
2
3
4
5
6
7
file="/path/to/file"

if [ -e "$file" ]; then
echo "$file exists"
else
echo "$file does not exist"
fi

布尔逻辑

test 支持逻辑运算符,可以将条件组合在一起:

  • -a:与操作(逻辑与)
  • -o:或操作(逻辑或)

在使用方括号时,可以使用双括号来更清晰地表示逻辑操作:

1
2
3
if [ -f "$file" -a -r "$file" ]; then
echo "$file exists and is readable"
fi

退出状态

test 的退出状态码很重要:

  • 返回 0 表示条件为真。
  • 返回非零状态码表示条件为假。

可以用 $? 来获取上一个命令的退出状态,例如:

1
2
3
4
test -e "$file"
if [ $? -eq 0 ]; then
echo "$file exists"
fi

expr

在 Linux shell 编程中,expr 是一个用于计算表达式的命令。它支持算术运算、逻辑运算和字符串操作。

基本功能

  • 算术计算: expr 可以执行基本的算术运算,包括加法、减法、乘法、除法和取余。例如:

    1
    2
    3
    4
    5
    expr 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
    4
    expr 5 = 5      # 结果是 1
    expr 5 != 3 # 结果是 1
    expr 5 \> 3 # 结果是 1
    expr 3 \< 5 # 结果是 1

逻辑运算

  • expr 支持逻辑运算符,例如并且(&)和或者(|):
    1
    2
    expr 1 \& 1     # 结果是 1
    expr 1 \| 0 # 结果是 1

注意事项

  • 在使用 expr 时,建议使用反斜杠(\)对某些符号(如 *&|)进行转义,因为它们在 shell 中有特殊含义。
  • 在现代脚本中,许多功能可以用更强大的工具(如 bcawk 或者 bash 内置的算术扩展 $(( ... )))来替代,expr 的使用相对较少。

示例

一个简单的示例,计算两个数的和并输出:

1
2
3
4
a=5
b=10
sum=$(expr $a + $b)
echo "Sum is: $sum" # 输出: Sum is: 15

总之,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

示例

一个简单示例,替换文件中的所有 helloworld,并打印输出:

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
2
3
4
5
6
7
if [ condition ]; then
# commands if condition is true
elif [ another_condition ]; then
# commands if another condition is true
else
# commands if all conditions are false
fi

示例:

1
2
3
4
5
6
7
if [ $age -ge 18 ]; then
echo "Adult"
elif [ $age -ge 13 ]; then
echo "Teenager"
else
echo "Child"
fi

while

while 语句在条件为真时重复执行代码块。基本语法如下:

1
2
3
while [ condition ]; do
# commands to be executed
done

示例:

1
2
3
4
5
count=1
while [ $count -le 5 ]; do
echo "Count is $count"
((count++))
done

for

for 语句用于遍历列表中的每个元素。基本语法如下:

1
2
3
for variable in list; do
# commands to be executed
done

示例:

1
2
3
for fruit in apple banana cherry; do
echo "I like $fruit"
done

也可以使用 C 样式的 for 循环:

1
2
3
for (( i=1; i<=5; i++ )); do
echo "Number $i"
done

case

case 语句用于根据变量的值匹配多个模式。基本语法如下:

1
2
3
4
5
6
7
8
9
10
11
case variable in
pattern1)
# commands for pattern1
;;
pattern2)
# commands for pattern2
;;
*)
# commands for unmatched patterns
;;
esac

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case $day in
1)
echo "Monday"
;;
2)
echo "Tuesday"
;;
3)
echo "Wednesday"
;;
*)
echo "Other day"
;;
esac

until

until 语句与 while 相反,它在条件为假时重复执行代码块。基本语法如下:

1
2
3
until [ condition ]; do
# commands to be executed
done

示例:

1
2
3
4
5
count=1
until [ $count -gt 5 ]; do
echo "Count is $count"
((count++))
done

select

select 语句用于生成菜单,让用户选择。基本语法如下:

1
2
3
select variable in list; do
# commands to be executed
done

示例:

1
2
3
4
select color in red green blue; do
echo "You selected $color"
break # 使用 break 退出 select 循环
done

函数

在 Linux shell 编程中,函数是一种将一组命令封装在一起的机制,允许你在脚本中重复调用这些命令。使用函数可以使脚本更结构化、可读性更强,并且可以避免代码重复。以下是对 Shell 函数的详解,包括定义、调用、参数、返回值和作用域等内容。

定义函数

在 Bash 中,定义函数的基本语法如下:

1
2
3
function_name () {
# commands to be executed
}

或者使用 function 关键字:

1
2
3
function function_name {
# commands to be executed
}

函数示例

1
2
3
4
5
greet() {
echo "Hello, $1!"
}

greet "Alice" # 输出: Hello, Alice!

函数参数

在函数内部,可以使用 $1$2 等来访问传递给函数的参数。$1 代表第一个参数,$2 代表第二个参数,以此类推。$@$* 用于访问所有参数。

1
2
3
4
5
6
add() {
sum=$(( $1 + $2 ))
echo "Sum is: $sum"
}

add 3 5 # 输出: Sum is: 8

返回值

函数的返回值使用return命令,通常返回值是一个整数,范围从 0 到 255。也可以通过 echo 命令返回输出。

1
2
3
4
5
6
7
multiply() {
return $(( $1 * $2 ))
}

multiply 4 5
result=$?
echo "Return value is: $result" # 输出: Return value is: 20

通过 echo 返回输出的示例:

1
2
3
4
5
6
get_date() {
echo "$(date)"
}

current_date=$(get_date) # 通过命令替换获取当前日期
echo "Current date is: $current_date"

本地变量与全局变量

  • 全局变量:在函数外部定义的变量在函数内部也是可访问的。
  • 局部变量:使用 local 关键字定义的变量只在该函数内部可见。
1
2
3
4
5
6
7
8
9
10
counter=0

increment() {
local counter=5 # 局部变量
((counter++))
echo "Counter inside function: $counter" # 输出: Counter inside function: 5
}

increment
echo "Counter outside function: $counter" # 输出: Counter outside function: 0

函数的调用

函数可以在定义后随时调用。可以在脚本的任何位置调用已定义的函数。

1
2
3
4
5
say_hello() {
echo "Hello!"
}

say_hello # 输出: Hello!

函数的作用域

函数的作用域通常存在以下几种情况:

  • 在同一脚本中的任意位置都可以调用已经定义的函数。
  • 如果在当前 shell 中定义的函数,也可以在子 shell 中调用,但局部变量不被共享。

递归函数

函数也可以调用自身,形成递归。这在某些算法(如计算阶乘)中非常有用。

1
2
3
4
5
6
7
8
9
10
factorial() {
if [ $1 -le 1 ]; then
echo 1
else
echo $(( $1 * $(factorial $(( $1 - 1 )) ) ))
fi
}

result=$(factorial 5)
echo "Factorial of 5 is: $result" # 输出: Factorial of 5 is: 120

函数的文档和帮助

为了使脚本更易于维护,可以在函数内部添加注释,或者使用 help 命令输出帮助信息。

1
2
3
4
5
6
my_function() {
# This function does something
echo "Doing something"
}

help 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
2
3
4
5
6
7
8
# my_functions.sh
greet() {
echo "Hello, $1!"
}

farewell() {
echo "Goodbye, $1!"
}

我们可以在另一个脚本 main.sh 中包含这个文件,并使用这些函数:

1
2
3
4
5
6
7
8
9
# main.sh
#!/bin/bash

# 包含自定义函数文件
source my_functions.sh

# 使用函数
greet "Alice"
farewell "Bob"

当你运行 main.sh 时,输出将是:

1
2
Hello, Alice!
Goodbye, Bob!

条件包含

有时,您可能希望仅在某个条件下包含某个文件。这可以使用 if 语句结合 [ -f filename ] 来实现,以确保文件存在。

1
2
3
if [ -f "my_functions.sh" ]; then
source my_functions.sh
fi

变量的作用域

通过 source 或点命令包含的文件中的变量将保留在当前的 shell 环境中。这意味着,您可以在包含的脚本中定义变量,并在主脚本中访问这些变量。

1
2
# my_variables.sh
my_var="Hello, World!"
1
2
3
# main.sh
source my_variables.sh
echo $my_var # 输出: Hello, World!

错误处理

如果您尝试包含一个不存在的文件,source 命令将不会抛出错误,但上面的代码将继续执行。这可能会导致难以跟踪的错误。您可以在包含文件之前检查文件是否存在。

1
2
3
4
5
6
if [ -f "my_functions.sh" ]; then
source my_functions.sh
else
echo "Error: my_functions.sh not found."
exit 1
fi

项目构建工具

项目构建时很重要但也相对简单的一步,无论使用哪款构建工具,都能使用AGI都能很轻松的写出他们的构建脚本,但我们还是要学会他们的语言和优劣,以便在AGI出现幻觉时解决这些问题,学习一些知识时比如QT,也离不开这些工具。

Makefile

Makefile只适用于一些中小型项目,它的代码可读性非常差,前期学起来很麻烦。在构建速度上,比不上CMake这些现代构建工具(虽然CMake也要先生成Makefile, 但在生成前做了很多优化和检查,但CMake只适合C/C++),建议使用CMakeSCons这种现代构建工具,老项目,想继续维护的可以选择性的使用新构建工具。这里之所以讲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
2
3
4
targets: prerequisites
command
command
command
  • targets (目标) 是文件名,用空格分隔。 通常,每个rule只有一个。
  • commands (命令) 是通常用于创建目标的一系列步骤。 这些 需要以制表符 开头,不可以是空格。
  • prerequisites(先决条件) 也是文件名,用空格分隔。 在运行目标的命令之前,这些文件需要存在。 这些也称为 dependencies(依赖项)

例如,要生成m.txt,依赖a.txtb.txt,规则如下:

我们举个例子:在当前目录下,有3个文本文件:a.txtb.txtc.txt

现在,我们要合并a.txtb.txt,生成中间文件m.txt,再用中间文件m.txtc.txt合并,生成最终的目标文件x.txt,整个逻辑如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────┐ ┌─────┐ ┌─────┐
│a.txt│ │b.txt│ │c.txt│
└─────┘ └─────┘ └─────┘
│ │ │
└───┬───┘ │
│ │
▼ │
┌─────┐ │
│m.txt│ │
└─────┘ │
│ │
└─────┬─────┘


┌─────┐
│x.txt│
└─────┘

根据上述逻辑,我们来编写Makefile

1
2
3
# 目标文件: 依赖文件1 依赖文件2
m.txt: a.txt b.txt
cat a.txt b.txt > m.txt

注意:Makefile的规则中,命令必须以Tab开头,不能是空格。

一条规则的格式为目标文件: 依赖文件1 依赖文件2 ...,紧接着,以Tab开头的是命令,用来生成目标文件。上述第一条规则使用cat命令合并了a.txtb.txt,并写入到m.txt,第二条规则。用什么方式生成目标文件make并不关心,因为命令完全是我们自己写的,可以是编译命令,也可以是cpmv等任何命令。

#开头的是注释,会被make命令忽略。

然后我们运行 make m.txt。 只要m.txt文件不存在,命令就会运行。 如果 m.txt 已存在,则不会运行任何命令。

需要注意的是,我所说的m.txt既是一个目标,也是一个文件。 那是因为两者直接绑在一起。 通常,当目标运行时(也就是运行目标的命令时),这些命令将创建一个与目标同名的文件(也就是 cat a.txt b.txt > m.txt)。

make使用文件的创建和修改时间来判断是否应该更新一个目标文件。


1
2
3
4
5
x.txt: m.txt c.txt
cat m.txt c.txt > x.txt

m.txt: a.txt b.txt
cat a.txt b.txt > m.txt

当我们执行make时,由于没有将目标作为参数提供给make命令,因此会运行第一个目标,也就是创建x.txt,但是由于x.txt依赖的文件m.txt不存在(另一个依赖c.txt已存在),故需要先执行规则m.txt创建出m.txt文件,再执行规则x.txt。执行完成后,当前目录下生成了两个文件m.txtx.txt

Makefile定义了一系列规则,每个规则在满足依赖文件的前提下执行命令,就能创建出一个目标文件。把默认执行的规则放第一条,其他规则的顺序是无关紧要的,因为make执行时自动判断依赖。此外,make会打印出执行的每一条命令,便于我们观察执行顺序以便调试。

伪目标

clean通常用作删除其他目标的输出的目标,但它在make中并不是一个专有的词。 你可以在此运行 makemake clean 来创建和删除 some_file

请注意,clean在这里做了两件新的事情:

  • 它不是第一个目标 (默认目标),也不是先决条件。 这意味着除非显式调用make clean,否则它永远不会运行
  • 它不是一个文件名。 如果你碰巧有一个名为 “clean” 的文件,则此目标将无法运行,这不是我们想要的。
1
2
3
4
5
some_file: 
touch some_file

clean:
rm -f some_file

.PHONY 添加到目标将防止Make将phony(假)目标与文件名混淆。 在此示例中,如果创建了文件 “clean”,则仍将运行 make clean。 从技术上讲,我应该在每个带有allclean的示例中使用它。 此外,”phony” 目标通常具有很少是文件名的名称,实际上,许多人忽略了它。

1
2
3
4
5
6
7
8
some_file:
touch some_file
touch clean

.PHONY: clean
clean:
rm -f some_file
rm -f clean

大型项目通常会提供cleaninstall这些约定俗成的伪目标名称,方便用户快速执行特定任务。

一般来说,并不需要用.PHONY标识clean等约定俗成的伪目标名称,除非有人故意搞破坏,手动创建名字叫clean的文件。

命令和执行

命令回显/静默

在命令之前添加 @ 以阻止其打印
你还可以运行带有-s的make命令,等同于在每行前面添加一个@

1
2
3
all: 
@echo "This make line will not be printed"
echo "But this will"

命令执行

每个命令都在一个新的shell中运行(或者至少效果是这样的)

1
2
3
4
5
6
7
8
9
10
11
all: 
cd ..
# 上面的cd不会影响该行,因为每个命令都有效地在新的shell中运行
echo `pwd`

# 此cd命令会影响下一个命令,因为它们在同一行上
cd ..;echo `pwd`

# 同上
cd ..; \
echo `pwd`

默认Shell

默认Shell是 ‘/bin/sh’。你可以通过更改变量SHELL来更改此设置:

1
2
3
4
SHELL=/bin/bash

cool:
echo "Hello from bash"

-k-i-错误处理

在运行时添加 -k,即使面对错误也要继续运行。 如果你想一次查看Make的所有错误,这将非常有用。
在命令前添加 - 以抑制错误
添加-i以使每个命令都会发生这种情况。

1
2
3
4
one:
# 此错误将被打印但被忽略,并且make将继续运行
-false
touch one

中断或杀死Make

备注:如果你对make按下ctrl+c,它将删除它刚刚创建的较新的目标。

Make的递归使用

要递归调用makefile,请使用特殊的 $(MAKE) 而不是 make,因为这样它才能为你传递make标志,并且本身不会受到它们的影响。

1
2
3
4
5
6
7
8
new_contents = "hello:\n\ttouch inside_file"
all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
cd subdir && $(MAKE)

clean:
rm -rf subdir

使用export进行递归make

export指令采用一个变量,并使sub-make命令可以访问它。 在本例中,导出了cooly,以便subdir中的Makefile可以使用它。

注意: export具有与sh相同的语法,但它们不相关 (尽管在功能上相似)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new_contents = "hello:\n\techo \$$(cooly)"

all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
@echo "---MAKEFILE CONTENTS---"
@cd subdir && cat makefile
@echo "---END MAKEFILE CONTENTS---"
cd subdir && $(MAKE)

# 请注意,变量和导出。 它们在全局范围内被设置/影响。
cooly = "The subdirectory can see me!"
export cooly
# 这将使上面的行无效: 取消导出cooly

clean:
rm -rf subdir

你需要导出变量,以便让它们也在shell中运行。

1
2
3
4
5
6
7
8
one=this will only work locally
export two=we can run subcommands with this

all:
@echo $(one)
@echo $$one
@echo $(two)
@echo $$two

.EXPORT_ALL_VARIABLES 为你导出所有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.EXPORT_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"

cooly = "The subdirectory can see me!"
# 这将使上面一行无效:取消导出cooly

all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
@echo "---MAKEFILE CONTENTS---"
@cd subdir && cat makefile
@echo "---END MAKEFILE CONTENTS---"
cd subdir && $(MAKE)

clean:
rm -rf subdir

make的参数

有一个很好的可以从Make运行的 options 列表。 查看 --dry-run--touch--old-file

你可以有多个目标来make,例如 make clean run test,运行clean,然后run,然后test

目标

all目标

要制作多个目标,而你想让所有目标都运行? 你可以制作一个all目标。 由于这是列出的第一个规则,因此如果调用 make 而未指定目标,则默认情况下它将运行。

1
2
3
4
5
6
7
8
9
10
11
all: one two three

one:
touch one
two:
touch two
three:
touch three

clean:
rm -f one two three

多目标

当一个规则有多个目标时,将为每个目标运行命令。 $@ 是包含目标名称的自动变量。

1
2
3
4
5
6
7
8
9
all: f1.o f2.o

f1.o f2.o:
echo $@
# 相当于:
# f1.o:
# echo f1.o
# f2.o:
# echo f2.o

自动变量和通配符

* 通配符

*% 在 Make中都被称为通配符,但它们的含义完全不同。 *会在你的文件系统中搜索匹配的文件名。 我建议你始终将其包装在wildcard 函数中,因为否则你可能会陷入下面描述的常见陷阱。

1
2
3
# 打印出每个.c文件的文件信息
print: $(wildcard *.c)
ls -la $?

“*” 可以在目标,先决条件或 wildcard 函数中使用。

危险:*不能在变量定义中直接使用

危险: 当 * 不匹配任何文件时,它将维持原样 (除非在wildcard 函数中运行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
thing_wrong := *.o # 不要这样做!‘*’不会展开
thing_right := $(wildcard *.o)

all: one two three four

# 失败,因为$(thing_wrong)是字符串“*.o”
one: $(thing_wrong)

# 如果没有与此模式匹配的文件,则保持为 *.o :(
two: *.o

# 如你所料!在这种情况下,它什么也不做。
three: $(thing_right)

# 与规则three相同
four: $(wildcard *.o)

% 通配符

%确实很有用,但由于它可以在多种情况下使用,所以有点令人困惑。

  • 在“匹配”模式下使用时,匹配字符串中的一个或多个字符。 这种匹配被称为词干(stem)。
  • 在“替换”模式下使用时,它会获取匹配的词干,并替换字符串中的词干。
  • % 最常用于规则定义和某些特定函数中。

自动变量

有很多个 自动变量 ,但通常只有几个出现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
hey: one two
# 输出 “hey”,因为这是目标名称
echo $@

# 输出比目标更新的所有先决条件
echo $?

# 输出所有先决条件
echo $^

# 表示第一个依赖文件,也就是这里的 $^ 的第一个元素。
echo $<

touch hey

one:
touch one

two:
touch two

clean:
rm -f hey one two

其它规则

隐式规则

Make偏爱c编译。 而每次它出现这种偏爱的时候,事情就会变得混乱。 也许Make中最令人困惑的部分是Make的魔术/自动规则(magic/automatic rules)。 Make会调用这些“隐含的”规则。 我个人不同意这个设计决定,也不建议使用它们,但是它们经常被使用,因此很有用。 以下是隐含规则的列表:

  • 编译C程序: 从 n.c 自动生成 n.o,命令形式为 $(CC) -c $(CPPFLAGS) $(CFLAGS)
  • 编译C++程序:n.on.ccn.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
2
3
4
5
6
7
8
9
10
11
12
CC = gcc # 隐式规则标志
CFLAGS = -g # 隐式规则的标志。打开调试信息

# 隐式规则 #1:blah是通过C链接器隐式规则构建的
# 隐式规则 #2: blah.o是通过C编译隐式规则构建的,因为blah.c存在
blah: blah.o

blah.c:
echo "int main() { return 0; }" > blah.c

clean:
rm -f blah*

静态模式规则

静态模式规则是在Makefile中减少编写量的另一种方式,但我会说更有用,并且 “魔术” 技巧更少。 它们的语法如下:

1
2
targets...: target-pattern: prereq-patterns ...
commands

本质是给定的 targettarget-pattern 匹配 (通过 % 通配符)。 任何匹配的东西都被称为 词干。 然后将词干替换为 “prereq-pattern”,以生成目标的prereq。

一个典型的用例是将.c文件编译成.o文件。 这里是手动方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
objects = foo.o bar.o all.o
all: $(objects)

# 这些文件通过上面隐式规则进行编译
foo.o: foo.c
bar.o: bar.c
all.o: all.c

all.c:
echo "int main() { return 0; }" > all.c

%.c:
touch $@

clean:
rm -f *.c *.o all

这是使用静态模式规则的更高效的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
objects = foo.o bar.o all.o
all: $(objects)

# 这些文件通过隐式规则进行编译
# 语法 - targets ...: target-pattern: prereq-patterns ...
# 在第一个目标foo.o的情况下,目标模式与foo.o匹配,并将“词干”设置为“foo”。
# 然后用该词干替换prereq模式中的 “%”
$(objects): %.o: %.c

all.c:
echo "int main() { return 0; }" > all.c

%.c:
touch $@

clean:
rm -f *.c *.o all

静态模式规则和过滤器

当我稍后介绍函数时,我将预示你可以使用它们来做什么。 filter 过滤器函数可以在静态模式规则中使用,以匹配正确的文件。 在本例中,我编造了.raw.result扩展名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

all: $(obj_files)

$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $<"
$(filter %.result,$(obj_files)): %.result: %.raw
echo "target: $@ prereq: $<"

%.c %.raw:
touch $@

clean:
rm -f $(src_files)

模式规则

模式规则经常被使用,但非常令人困惑。你可以从两个方面来看待它们:

  • 定义自己的隐含规则的方法
  • 静态模式规则的更简单形式

让我们首先从一个例子开始:

1
2
3
# 定义将每个.c文件编译为.o文件的模式规则
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

模式规则在目标中包含%。 此 % 匹配任何非空字符串,其他字符匹配自己。 模式规则先决条件中的%代表与目标中的%匹配的同一词干。

这里是另一个例子:

1
2
3
4
# 定义一个在先决条件中没有模式的模式规则。
# 这只会在需要时创建空的.c文件。
%.c:
touch $@

双冒号规则

很少使用双冒号规则,但用它允许为同一目标定义多个规则。 如果这些规则是单冒号,则会打印一条警告,并且只运行这里的第二组命令。

1
2
3
4
5
6
7
all: blah

blah::
echo "hello"

blah::
echo "hello again"

变量

类型和修饰

有两种类型的变量:

  • 递归 (使用 =) - 在使用命令时才会查找变量,而不在定义命令时查找变量。
  • 简单展开 (使用 :=) - 就像普通的命令式编程一样 – 只有到目前为止定义的变量才会得到展开
1
2
3
4
5
6
7
8
9
10
# 递归变量。这将能在下面打印出 “later”
one = one ${later_variable}
# 简单展开变量。这无法在下面打印出 “later”
two := two ${later_variable}

later_variable = later

all:
echo $(one)
echo $(two)

简单的说展开(使用:=)允许你追加到一个变量。 递归定义可能会给出无限循环错误。

1
2
3
4
5
6
one = hello
# one被定义为一个简单展开变量(:=),因此可以处理追加
one := ${one} there

all:
echo $(one)

?= 仅设置尚未设置的变量

1
2
3
4
5
6
7
one = hello
one ?= will not be set
two ?= will be set

all:
echo $(one)
echo $(two)

行尾的空格不会被去掉,但行首的空格会被去掉。 要使用单个空格制作变量,请使用 $(nullstring)

1
2
3
4
5
6
7
8
9
with_spaces = hello   # with_spaces在 "hello" 之后有很多空格
after = $(with_spaces)there

nullstring =
space = $(nullstring) # 构造一个单个空格变量。

all:
echo "$(after)"
echo start"$(space)"end

未定义的变量实际上是空字符串!

1
2
3
all: 
# 未定义的变量只是空字符串!
echo $(nowhere)

使用 += 追加

1
2
3
4
5
foo := start
foo += more

all:
echo $(foo)

字符串替换也是一种真正常见且有用的修改变量的方法。 另请查看 Text FunctionsFilename Functions

命令行参数和覆盖

你可以使用override覆盖来自命令行的变量。 在这里,我们用 make option_one=hi 运行 make

1
2
3
4
5
6
7
# 覆盖命令行参数
override option_one = did_override
# 未覆盖的命令行参数
option_two = not_override
all:
echo $(option_one)
echo $(option_two)

命令列表和define

“define”实际上只是一个命令列表。 它与函数无关。 请注意,这与命令之间的分号有点不同,因为每个命令都在单独的shell中运行,如预期的那样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
one = export blah="I was set!"; echo $$blah

define two
export blah=set
echo $$blah
endef

# one和two是不同的。

all:
@echo "这会打印 'I was set'"
@$(one)
@echo "这不会打印 'I was set' 因为每个command都在单独的shell中运行"
@$(two)

目标特定变量

可以为特定目标中赋值变量

1
2
3
4
5
6
7
all: one = cool

all:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

模式特定变量

你可以为特定目标模式中分配变量

1
2
3
4
5
6
7
%.c: one = cool

blah.c:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

条件

条件if/else

1
2
3
4
5
6
7
8
foo = ok

all:
ifeq ($(foo), ok)
echo "foo equals ok"
else
echo "nope"
endif

检查变量是否为空

1
2
3
4
5
6
7
8
9
10
nullstring =
foo = $(nullstring) # 行尾; 这里有一个空格

all:
ifeq ($(strip $(foo)),)
echo "foo is empty after being stripped"
endif
ifeq ($(nullstring),)
echo "nullstring doesn't even have spaces"
endif

检查是否定义了变量

ifdef不展开变量引用;它只查看是否定义了某些内容

1
2
3
4
5
6
7
8
9
10
bar =
foo = $(bar)

all:
ifdef foo
echo "foo is defined"
endif
ifndef bar
echo "but bar is not"
endif

$(makeflags)

此示例向你展示了如何使用 findstringMAKEFLAGS 测试make标志。 使用make -i运行此示例,以查看它打印出echo语句。

1
2
3
4
5
6
7
8
bar =
foo = $(bar)

all:
# 搜索 “-i” 标志。 MAKEFLAGS只是一个单一字符的列表,每个标志一个字符。 所以在这种情况下寻找 “i”。
ifneq (,$(findstring i, $(MAKEFLAGS)))
echo "i was passed to MAKEFLAGS"
endif

函数

首个函数

函数主要用于文本处理。 用 $(fn, arguments)${fn, arguments} 调用函数。 你可以使用 call 内置函数来创建自己的代码。 Make有相当数量的 内置函数

1
2
3
bar := ${subst not, totally, "I am not superman"}
all:
@echo $(bar)

如果要使用变量替换空格或逗号

1
2
3
4
5
6
7
8
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))

all:
@echo $(bar)

第一个参数后不要包含空格。 否则这将被视为字符串的一部分。

1
2
3
4
5
6
7
8
9
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space), $(comma) , $(foo))

all:
#输出为 ", a , b , c"。 注意引入的空格
@echo $(bar)

字符串替换

$(patsubst pattern,replacement,text)执行以下操作:

“在与pattern匹配的text中查找空格分隔的单词,并将其替换为replacement。 在这里,模式可以包含充当通配符的‘%’,匹配单词中任意数量的任何字符。 如果替换还包含 ‘%’,则将 ‘%’ 替换为与模式中的 ‘%’ 匹配的文本。 只有模式和替换中的第一个‘%’会以这种方式处理;任何后续的‘%’都不会改变。” (GNU docs)

替换引用 $(text:pattern=replacement) 是这方面的简写。

还有一个只替换后缀的简写:$(text:suffix=replacement)。 这里不使用 % 通配符。

注意:不要为此简写添加额外的空格。 它将被视为搜索或替换术语。

1
2
3
4
5
6
7
8
9
10
11
foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo))
# 这是上述内容的简写
two := $(foo:%.o=%.c)
# 这是仅后缀的简写,也等同于上述。
three := $(foo:.o=.c)

all:
echo $(one)
echo $(two)
echo $(three)

foreach函数

foreach函数如下所示: $(foreach var,list,text)。 它将一个单词列表(由空格分隔)转换为另一个列表。 var被设置为list中每个单词,同时text是针对每一个单词的展开。
这里在每个单词后附加了一个感叹号:

1
2
3
4
5
6
7
foo := who are you
# 对于foo中的每个“word”,输出相同的单词,并在后面加一个感叹号
bar := $(foreach wrd,$(foo),$(wrd)!)

all:
# 输出是 "who! are! you!"
@echo $(bar)

if函数

if检查第一个参数是否为非空。如果是,则运行第二个参数,否则运行第三个参数。

1
2
3
4
5
6
7
foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)

all:
@echo $(foo)
@echo $(bar)

调用函数

Make支持创建基本函数。 你只需创建一个变量即可定义函数,但需要使用$(0)$(1)等参数。 然后,你使用特殊的 ‘call’ 函数调用该函数。 语法为$(call variable,param,param)$(0) 是变量,而 $(1)$(2)… 等是参数。

1
2
3
4
5
sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)

all:
# 输出 “Variable Name:sweet_new_fn First: go Second: tigers Empty Variable:”
@echo $(call sweet_new_fn, go, tigers)

shell函数

shell - 这调用shell,但它用空格替换换行符!

1
2
all: 
@echo $(shell ls -la) # 很难看,因为换行不见了!

其他特性

包含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
2
3
4
5
6
7
8
9
10
11
12
13
14
vpath %.h ../headers ../other-directory

some_binary: ../headers blah.h
touch some_binary

../headers:
mkdir ../headers

blah.h:
touch ../headers/blah.h

clean:
rm -rf ../headers
rm -f some_binary

多行

反斜杠(“")字符使我们能够在命令太长时使用多行

1
2
3
some_file: 
echo This line is too long, so \
it is broken up into multiple lines

.phony

.PHONY 添加到目标将防止Make将phony(假)目标与文件名混淆。 在此示例中,如果创建了文件 “clean”,则仍将运行 make clean。 从技术上讲,我应该在每个带有allclean的示例中使用它,但我没有保持clean示例。 此外,”phony” 目标通常具有很少是文件名的名称,实际上,许多人忽略了它。

1
2
3
4
5
6
7
8
some_file:
touch some_file
touch clean

.PHONY: clean
clean:
rm -f some_file
rm -f clean

.delete_on_error

如果命令返回非零退出状态,则make工具将停止运行规则(并将传播回先决条件)。
如果规则以这种方式失败,则 DELETE_ON_ERROR 将删除规则的目标。 这将发生在所有目标上,而不仅仅是它之前的那个PHONY目标。 最好始终使用它是一个好主意,即使make出于历史原因没有默认使用这个策略。

1
2
3
4
5
6
7
8
9
10
.DELETE_ON_ERROR:
all: one two

one:
touch one
false

two:
touch two
false

示例

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
# 感谢Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/)
TARGET_EXEC := final_program

BUILD_DIR := ./build
SRC_DIRS := ./src

# 找到我们要编译的所有C和C++文件
# 请注意 * 表达式两边的单引号。 否则Make会在那里错误地展开。
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s')

# 每个C/C++文件的字符串替换。
# 例如,hello.cpp变成./build/hello.cpp.o
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)

# 字符串替换(不带%的后缀版本)。
#例如,./build/hello.cpp.o变成./build/hello.cpp.d
DEPS := $(OBJS:.o=.d)

# ./src中的每个文件夹将需要传递给GCC,以便它可以找到头文件
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
# 为INC_DIRS添加前缀。所以moduleA会变成-ImoduleA。GCC会理解-I标志
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

# -MMD和-MP标志一起为我们生成Makefiles!
# 这些文件将有.d而不是.o作为输出。
CPPFLAGS := $(INC_FLAGS) -MMD -MP

# 最后一个构建步骤。
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
$(CC) $(OBJS) -o $@ $(LDFLAGS)

# C源代码的构建步骤
$(BUILD_DIR)/%.c.o: %.c
mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# C++源代码构建步骤
$(BUILD_DIR)/%.cpp.o: %.cpp
mkdir -p $(dir $@)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@


.PHONY: clean
clean:
rm -r $(BUILD_DIR)

# 包含.d makefiles。
# 前面的 - 抑制缺少makefile的错误。
# 最初,所有的.d文件都将丢失,我们不希望出现这些错误。
-include $(DEPS)

多文件目录管理

在主Makefile中,我们可以这样定义一个递归构建规则:

1
2
3
all:
$(MAKE) -C src
$(MAKE) -C lib

在上述示例中,$(MAKE) -C src表示调用src目录下的Makefile构建目标;$(MAKE) -C lib表示调用lib目录下的Makefile构建目标。

自动查找子目录

我们可以通过wildcardforeach来自动查找所有子目录,并自动生成子目录规则。示例如下:

1
2
SUBDIRS := $(wildcard */)
include $(patsubst %/, %/Makefile, $(SUBDIRS))

在上述示例中,wildcard */会查找所有的子目录;patsubst %/, %/Makefile, $(SUBDIRS)会自动生成所有子目录对应的Makefile路径。

export

我们可以通过 export 导出变量供子目录使用。

1
2
CC := gcc
export CC

include

在Makefile使用include关键字可以把别的Makefile包含进来,这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。 比如命令

1
include file.dep

即把file.dep文件在当前Makefile文件中展开,亦即把file.dep文件的内容包含进当前Makefile文件

在 include前面可以有一些空字符,但是绝不能是[Tab]键开始。

常用模板

主Makefile

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
CC := gcc
INCLUDE_DIR := -I ../include
OBJ_DIR := ${shell pwd}/obj
BIN_DIR := ${shell pwd}/bin
SUB_DIR := main fun global obj
TARGET := my_exec

#导出为全局变量,给其他文件使用
export CC INCLUDE_DIR BIN_DIR OBJ_DIR SUB_DIR TARGET

all:$(SUB_DIR)

$(SUB_DIR) : MK_BIN
make -C $@


MK_BIN:
mkdir -p $(BIN_DIR)

clean:
rm -rf ./bin ./obj/*.o

install:
sudo cp $(BIN_DIR)/$(TARGET) /usr/bin

uninstall:
sudo rm -rf /usr/bin/$(TARGET)

fun、global、main文件夹使用同样的Makefile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#获取当前目录先的.c文件信息
SRC=$(wildcard *.c)
#准备把把当前目录下的.c替换为.o文件
OBJ=$(patsubst %c,%o,$(SRC))

all:$(OBJ)

#Makfile的静态模式 :
# main.o : main.c
# gcc -I ../include/ -c $^ -o ^@
#
$(OBJ) : %.o:%.c
$(CC) $(INCLUDE_DIR) -c $^ -o $(OBJ_DIR)/$@

obj目录

1
2
$(BIN_DIR)/$(TARGET) : $(wildcard *.o)
$(CC) $(INCLUDE_DIR) $^ -o $@

Kbuild/Kconfig

CMake

虽然CMake是基于Makefile的,但是相较于 MakefileCMake更适用于大型项目,并且更易于维护,构建速度,可读性也比Makefile好很多(虽然CMake也要先生成Makefile, 但在生成前做了很多优化和检查)。Makefile适用于一些中小型项目。

linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:

  1. CMake 配置文件 CMakeLists.txt
  2. 执行命令 cmake PATH 或者 ccmake

PATH 生成 Makefile(ccmakecmake 的区别在于前者提供了一个交互式的界面)。其中, PATHCMakeLists.txt 所在的目录。

也可以使用 make 命令进行编译。

注意:虽然 CMake 支持大写、小写和混合大小写的命令,但首选小写命令,本教程将始终使用小写命令。

参考链接

https://cmake.com.cn/cmake/help/latest/guide/tutorial/A%20Basic%20Starting%20Point.html

单个源文件

最基本的 CMake 项目是从单个源代码文件构建的可执行文件。对于这样的简单项目,仅需要一个包含三个命令的 CMakeLists.txt 文件。

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.10)

# set the project name
project(tutorial)

# add the executable
add_executable(tutorial tutorial.cpp)

任何项目的顶级 CMakeLists.txt 必须首先使用 cmake_minimum_required() 命令指定 CMake 的最低版本。这将建立策略设置并确保以下 CMake 函数与兼容版本的 CMake 一起运行。

要启动项目,我们使用 project() 命令设置项目名称。此调用对于每个项目都是必需的,并且应在 cmake_minimum_required() 之后立即调用。正如我们稍后将看到的那样,此命令还可用于指定其他项目级信息,例如语言或版本号。

最后,add_executable() 命令告诉 CMake 使用指定的源代码文件创建可执行文件。

构建,编译和运行

推荐使用外部构建,把生成文件和项目文件分开来构建,内部构建会破坏原有代码结构。

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
# -----------------------------------------------
mkdir build
cd build
# 构建系统是需要指定 CMakeLists.txt 所在路径,此时在 build 目录下,所以用 .. 表示 CMakeLists.txt 在上一级目录。
➜ cmake ..
# 此时在 build 目录下会生成 Makefile 文件,然后调用编译器来实际编译和链接项目:
➜ cmake --build .
# --build 指定编译生成的文件存放目录,其中就包括可执行文件,. 表示存放到当前目录,
# 在 build 目录下生成了一个 Tutorial.exe 可执行文件
# -----------------------------------------------
➜ process tree
.
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ │ ├── 3.31.2
│ │ │ ├── CMakeCCompiler.cmake
│ │ │ ├── CMakeCXXCompiler.cmake
│ │ │ ├── CMakeDetermineCompilerABI_C.bin
│ │ │ ├── CMakeDetermineCompilerABI_CXX.bin
│ │ │ ├── CMakeSystem.cmake
│ │ │ ├── CompilerIdC
│ │ │ │ ├── a.out
│ │ │ │ ├── CMakeCCompilerId.c
│ │ │ │ └── tmp
│ │ │ └── CompilerIdCXX
│ │ │ ├── a.out
│ │ │ ├── CMakeCXXCompilerId.cpp
│ │ │ └── tmp
│ │ ├── cmake.check_cache
│ │ ├── CMakeConfigureLog.yaml
│ │ ├── CMakeDirectoryInformation.cmake
│ │ ├── CMakeScratch
│ │ ├── Makefile2
│ │ ├── Makefile.cmake
│ │ ├── pkgRedirects
│ │ ├── progress.marks
│ │ ├── TargetDirectories.txt
│ │ └── tutorial.dir
│ │ ├── build.make
│ │ ├── cmake_clean.cmake
│ │ ├── compiler_depend.make
│ │ ├── compiler_depend.ts
│ │ ├── DependInfo.cmake
│ │ ├── depend.make
│ │ ├── flags.make
│ │ ├── link.txt
│ │ ├── progress.make
│ │ ├── tutorial.cpp.o
│ │ └── tutorial.cpp.o.d
│ ├── cmake_install.cmake
│ ├── Makefile
│ └── tutorial
├── CMakeLists.txt
└── tutorial.cpp

10 directories, 33 files

Windows 下,CMake 默认使用微软的 MSVC 作为编译器,想使用 MinGW 编译器,可以通过 -G 参数来进行指定,只有第一次构建项目时需要指定。

1
cmake -G "MinGW Makefiles" ..

优化 CMakeLists.txt 文件

set 与 PROJECT_NAME

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.15)

# set the project name
project(Tutorial)

SET(SRC_LIST tutorial.cpp)

# add the executable
add_executable(${PROJECT_NAME} ${SRC_LIST})

指定了项目名后,后面可能会有多个地方用到这个项目名,如果更改了这个名字,就要改多个地方,比较麻烦,那么可以使用 PROJECT_NAME 来表示项目名。

1
add_executable(${PROJECT_NAME} tutorial.cpp)

生成可执行文件需要指定相关的源文件,如果有多个,那么就用空格隔开,比如:

1
add_executable(${PROJECT_NAME} a.cpp b.cpp c.cpp)

我们也可以用一个变量来表示这多个源文件:

1
2
set(SRC_LIST a.cpp b.cpp c.cpp)
add_executable(${PROJECT_NAME} ${SRC_LIST})

set 命令指定 SRC_LIST 变量来表示多个源文件,用 ${var_name} 获取变量的值。


添加版本号和配置头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.15)

# 使用 project 命令设置项目名称和版本号。
project(Tutorial VERSION 1.0.2)

SET(SRC_LIST tutorial.cpp)

# add the executable
add_executable(${PROJECT_NAME} ${SRC_LIST})

# 配置头文件将版本号传递给源代码
configure_file(TutorialConfig.h.in TutorialConfig.h)

# 添加自动搜索路径
target_include_directories(${PROJECT_NAME} PUBLIC ${PROJECT_BINARY_DIR})

由于 TutorialConfig.h 文件这里被设置为自动写入 build 目录,因此需要将该目录添加到搜索头文件的路径列表中,也可以修改为写到其它目录。

PROJECT_BINARY_DIR 表示当前工程的二进制路径,即编译产物会存放到该路径,此时PROJECT_BINARY_DIR 就是 build 所在路径。

手动创建 TutorialConfig.h.in 文件,包含以下内容:

1
2
3
4
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define Tutorial_VERSION_PATCH @PROJECT_VERSION_PATCH@

当使用 CMake 构建项目后,会在 build 中生成一个 TutorialConfig.h 文件,内容如下:

1
2
3
4
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR 1
#define Tutorial_VERSION_MINOR 0
#define Tutorial_VERSION_PATCH 2

下一步在 tutorial.cpp 包含头文件 TutorialConfig.h,最后通过以下代码打印出可执行文件的名称和版本号。

1
2
3
4
5
6
7
if (argc < 2) {
// report version
std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
<< Tutorial_VERSION_MINOR << std::endl;
std::cout << "Usage: " << argv[0] << " number" << std::endl;
return 1;
}

添加编译时间戳

有时候我们需要知道编译时的时间戳,并在程序运行时打印出来。那就需要在 CMakeLists.txt 中添加如下这句:

1
string(TIMESTAMP COMPILE_TIME %Y%m%d-%H%M%S)

这表示将时间戳已指定格式保存到 COMPILE_TIME 变量中。

然后修改上面的 TutorialConfig.h.in 文件:

1
2
3
4
5
6
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define Tutorial_VERSION_PATCH @PROJECT_VERSION_PATCH@

#define TIMESTAMP @COMPILE_TIME@

在构建项目后,TutorialConfig.h 文件就会自动增加一句:

1
#define TIMESTAMP 20230220-203532

这样就可以在源码中打印出 TIMESTAMP 的值了。

也可以直接通过 __TIMESTAMP__ 变量打印时间戳。

指定 C/C++ 标准

我们可以在CMake代码中设置正确的标志,以启动对特定C/C++标准的支持。最简单的是使用CMAKE_CXX_STANDARD变量。

注意:要在add_executable之前,添加对CMAKE_CXX_STANDARD的声明。

脚本中set是将普通变量、缓存变量或者环境变量设置为指定的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#启动对C++14标准的支持
set(CMAKE_CXX_STANDARD 14)

# 显式要求指明支持C++标准
set(CMAKE_CXX_STANDARD_REQUIRED True)

# 启动对C11标准的支持
set(CMAKE_C_STANDARD 11)

# 显式要求指明支持C标准
set(CMAKE_C_STANDARD_REQUIRED True)

#设置可执行程序
add_executable(${PROJECT_NAME} ${SRC_LIST})

添加库

现在我们将向项目中添加一个库,这个库包含计算数字平方根的实现,可执行文件使用这个库,而不是编译器提供的标准平方根函数。

我们把库放在名为 MathFunctions 的子目录中。此目录包含头文件 MathFunctions.h 和源文件 mysqrt.cpp。源文件有一个名为 mysqrt 的函数,它提供了与编译器的 sqrt 函数类似的功能,MathFunctions.h 则是该函数的声明。

在 MathFunctions 目录下创建一个 CMakeLists.txt 文件,并添加以下一行:

1
2
# MathFunctions/CMakeLists.txt
add_library(MathFunctions mysqrt.cpp)

表示添加一个叫 MathFunctions 的库文件。

CMake 中的 target 有可执行文件和库文件,分别使用 add_executableadd_library 命令生成,除了指定生成的可执行文件名/库文件名,还需要指定相关的源文件。

此时文件结构为:

1
2
3
4
5
6
7
8
9
Demo/
build/
MathFunctions/
CMakeLists.txt
MathFunctions.h
mysqrt.cpp
CMakeLists.txt
tutorial.cpp
TutorialConfig.h.in

为了使用 MathFunctions 这个库,我们将在顶级 CMakeLists.txt 文件中添加一个 add_subdirectory(MathFunctions) 命令指定库所在子目录,该子目录下应包含 CMakeLists.txt 文件和代码文件。

可执行文件要使用库文件,需要能够找到库文件和对应的头文件,可以分别通过 target_link_librariestarget_include_directories 来指定。

使用 target_link_libraries 将新的库文件添加到可执行文件中,使用 target_include_directories 将 MathFunctions 添加为头文件目录,添加到 Tutorial 目标上,以便 mysqrt.h 可以被找到。

顶级 CMakeLists.txt 的最后几行如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(${PROJECT_NAME} tutorial.cpp)

target_link_libraries(${PROJECT_NAME} PUBLIC MathFunctions)

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(${PROJECT_NAME} PUBLIC
${PROJECT_BINARY_DIR}
${PROJECT_SOURCE_DIR}/MathFunctions
)

MathFunctions 库就算添加完成了,接下来就是在主函数使用该库中的函数,先在 tutorial.cpp 文件中添加头文件:

1
#include "MathFunctions.h"

然后使用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES ${PROJECT_SOURCE_DIR}/MathFunctions)
endif()

# add the executable
add_executable(${PROJECT_NAME} tutorial.cpp)

target_link_libraries(${PROJECT_NAME} PUBLIC ${EXTRA_LIBS})

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(${PROJECT_NAME} PUBLIC
${PROJECT_BINARY_DIR}
${EXTRA_INCLUDES}
)

在 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
2
3
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif

然后,还在 tutorial.cpp 中,使用 USE_MYMATH 选择使用哪个平方根函数:

1
2
3
4
5
#ifdef USE_MYMATH
const double outputValue = mysqrt(inputValue);
#else
const double outputValue = sqrt(inputValue);
#endif

因为源代码使用了 USE_MYMATH 宏,可以用下面的行添加到 tutorialconfig.h.in 文档中:

1
2
// TutorialConfig.h.in
#cmakedefine USE_MYMATH

现在使用 cmake 命令构建项目,并运行生成的 Tutorial 可执行文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
build> cmake -G"MinGW Makefiles" ..
build> cmake --build .
build> Tutorial.exe 8
Computing sqrt of 8 to be 4.5
Computing sqrt of 8 to be 3.13889
Computing sqrt of 8 to be 2.84378
Computing sqrt of 8 to be 2.82847
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
Computing sqrt of 8 to be 2.82843
The square root of 8 is 2.82843

默认调用 mysqrt 函数,也可以在构建项目时指定 USE_MYMATH 的值为 OFF:

1
2
> cmake -DUSE_MYMATH=OFF ..
> cmake --build .

此时会调用自带的 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
2
3
4
# MathFunctions/CMakeLists.txt
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)

CMAKE_CURRENT_SOURCE_DIR 表示 MathFunctions 库所在目录。

现在我们已经为 MathFunctions 指定了使用要求 INTERFACE,那么可以从顶级 CMakeLists.txt 中删除EXTRA_INCLUDES变量的相关使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(USE_MYMATH)
add_subdirectory(MathFunctions)
list(APPEND EXTRA_LIBS MathFunctions)
list(APPEND EXTRA_INCLUDES ${PROJECT_SOURCE_DIR}/MathFunctions) # 删除此行
endif()

...

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(${PROJECT_NAME} PUBLIC
${PROJECT_BINARY_DIR}
${EXTRA_INCLUDES} # 删除此行
)

现在只要是链接了 MathFunctions 库,就会自动包含 MathFunctions 所在目录的头文件,简洁而优雅。

这里补充两个知识点:

1、使用要求除了 INTERFACE,还有PRIVATEPUBLICINTERFACE表示消费者需要生产者不需要,PRIVATE表示消费者不需要生产者需要,PUBLIC 表示消费者和生产者都需要。

2、这里使用 add_library 命令生成的 MathFunctions 库其实是静态链接库。

build 目录介绍

在文本中,我都是创建了一个 build 用来存放 cmake 构建和编译的产物,这里简单说下里面有些什么东西。

1
2
3
4
5
6
7
8
build/
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
Tutorial.exe
TutorialConfig.h
MathFunctions/

其中 Makefile 是 cmake 根据顶级 CMakeLists.txt 生成的构建文件,通过该文件可以对整个项目进行编译。

Tutorial.exe 就是生成的可执行文件,通过该文件运行程序。

TutorialConfig.h 是用于配置信息的头文件,是 cmake 根据 TutorialConfig.h.in 文件自动生成的。

还有个 MathFunctions 文件夹:

1
2
3
4
5
MathFunctions/
CMakeFiles/
cmake_install.cmake
Makefile
libMathFunctions.a

其中 Makefile 是 cmake 根据 MathFunctions 目录下的 CMakeLists.txt 生成的构建文件。

libMathFunctions.a 则是 MathFunctions 静态链接库,可执行文件会通过这个库调用 mysqrt 函数。

安装和测试

CMake 也可以指定安装规则,以及添加测试。这两个功能分别可以通过在产生 Makefile 后使用 make installmake test 来执行。在以前的 GNU Makefile 里,你可能需要为此编写 installtest 两个伪目标和相应的规则,但在 CMake 里,这样的工作同样只需要简单的调用几条命令。

定制安装规则

首先先在 math/CMakeLists.txt 文件里添加下面两行:

1
2
3
# 指定 MathFunctions 库的安装路径
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

指明 MathFunctions 库的安装路径。之后同样修改根目录的 CMakeLists 文件,在末尾添加下面几行:

1
2
3
4
# 指定安装路径
install (TARGETS Demo DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/config.h"
DESTINATION include)

通过上面的定制,生成的 Demo 文件和 MathFunctions 函数库 libMathFunctions.o 文件将会被复制到 /usr/local/bin 中,而 MathFunctions.h 和生成的 config.h 文件则会被复制到 /usr/local/include 中。我们可以验证一下(顺带一提的是,这里的 /usr/local/ 是默认安装到的根目录,可以通过修改 CMAKE_INSTALL_PREFIX 变量的值来指定这些文件应该拷贝到哪个根目录):

1
2
3
4
5
6
7
8
9
10
11
12
13
[ehome@xman Demo5]$ sudo make install
[ 50%] Built target MathFunctions
[100%] Built target Demo
Install the project...
-- Install configuration: ""
-- Installing: /usr/local/bin/Demo
-- Installing: /usr/local/include/config.h
-- Installing: /usr/local/bin/libMathFunctions.a
-- Up-to-date: /usr/local/include/MathFunctions.h
[ehome@xman Demo5]$ ls /usr/local/bin
Demo libMathFunctions.a
[ehome@xman Demo5]$ ls /usr/local/include
config.h MathFunctions.h

为工程添加测试

添加测试同样很简单。CMake 提供了一个称为 CTest 的测试工具。我们要做的只是在项目根目录的 CMakeLists 文件中调用一系列的 add_test 命令。

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
# 启用测试
enable_testing()

# 测试程序是否成功运行
add_test (test_run Demo 5 2)

# 测试帮助信息是否可以正常提示
add_test (test_usage Demo)
set_tests_properties (test_usage
PROPERTIES PASS_REGULAR_EXPRESSION "Usage: .* base exponent")

# 测试 5 的平方
add_test (test_5_2 Demo 5 2)

set_tests_properties (test_5_2
PROPERTIES PASS_REGULAR_EXPRESSION "is 25")

# 测试 10 的 5 次方
add_test (test_10_5 Demo 10 5)

set_tests_properties (test_10_5
PROPERTIES PASS_REGULAR_EXPRESSION "is 100000")

# 测试 2 的 10 次方
add_test (test_2_10 Demo 2 10)

set_tests_properties (test_2_10
PROPERTIES PASS_REGULAR_EXPRESSION "is 1024")

上面的代码包含了四个测试。第一个测试 test_run 用来测试程序是否成功运行并返回 0 值。剩下的三个测试分别用来测试 5 的 平方、10 的 5 次方、2 的 10 次方是否都能得到正确的结果。其中 PASS_REGULAR_EXPRESSION 用来测试输出是否包含后面跟着的字符串。让我们看看测试的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[ehome@xman Demo5]$ make test
Running tests...
Test project /home/ehome/Documents/programming/C/power/Demo5
Start 1: test_run
1/4 Test #1: test_run ......................... Passed 0.00 sec
Start 2: test_5_2
2/4 Test #2: test_5_2 ......................... Passed 0.00 sec
Start 3: test_10_5
3/4 Test #3: test_10_5 ........................ Passed 0.00 sec
Start 4: test_2_10
4/4 Test #4: test_2_10 ........................ Passed 0.00 sec

100% tests passed, 0 tests failed out of 4

Total Test time (real) = 0.01 sec

如果要测试更多的输入数据,像上面那样一个个写测试用例未免太繁琐。这时可以通过编写宏来实现:

1
2
3
4
5
6
7
8
9
10
11
# 定义一个宏,用来简化测试工作
macro (do_test arg1 arg2 result)
add_test (test_${arg1}_${arg2} Demo ${arg1} ${arg2})
set_tests_properties (test_${arg1}_${arg2}
PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro (do_test)

# 使用该宏进行一系列的数据测试
do_test (5 2 "is 25")
do_test (10 5 "is 100000")
do_test (2 10 "is 1024")

关于 CTest 的更详细的用法可以通过 man 1 ctest 参考 CTest 的文档。

生成安装包

本节将学习如何配置生成各种平台上的安装包,包括二进制安装包和源码安装包。为了完成这个任务,我们需要用到 CPack ,它同样也是由 CMake 提供的一个工具,专门用于打包。首先在顶层的 CMakeLists.txt 文件尾部添加下面几行:

1
2
3
4
5
6
7
# 构建一个 CPack 安装包
include (InstallRequiredSystemLibraries)
set (CPACK_RESOURCE_FILE_LICENSE
"${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set (CPACK_PACKAGE_VERSION_MAJOR "${Demo_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${Demo_VERSION_MINOR}")
include (CPack)

上面的代码做了以下几个工作:

  1. 导入 InstallRequiredSystemLibraries 模块,以便之后导入 CPack 模块;
  2. 设置一些 CPack 相关变量,包括版权信息和版本信息,其中版本信息用了上一节定义的版本号;
  3. 导入 CPack 模块。

接下来的工作是像往常一样构建工程,并执行 cpack 命令。

  • 生成二进制安装包:
1
cpack -C CPackConfig.cmake
  • 生成源码安装包
1
cpack -C CPackSourceConfig.cmake

我们可以试一下。在生成项目后,执行 cpack -C CPackConfig.cmake 命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[ehome@xman Demo8]$ cpack -C CPackSourceConfig.cmake
CPack: Create package using STGZ
CPack: Install projects
CPack: - Run preinstall target for: Demo8
CPack: - Install project: Demo8
CPack: Create package
CPack: - package: /home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux.sh generated.
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: Demo8
CPack: - Install project: Demo8
CPack: Create package
CPack: - package: /home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux.tar.gz generated.
CPack: Create package using TZ
CPack: Install projects
CPack: - Run preinstall target for: Demo8
CPack: - Install project: Demo8
CPack: Create package
CPack: - package: /home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux.tar.Z generated.

此时会在该目录下创建 3 个不同格式的二进制包文件:

1
2
[ehome@xman Demo8]$ ls Demo8-*
Demo8-1.0.1-Linux.sh Demo8-1.0.1-Linux.tar.gz Demo8-1.0.1-Linux.tar.Z

这 3 个二进制包文件所包含的内容是完全相同的。我们可以执行其中一个。此时会出现一个由 CPack 自动生成的交互式安装界面:

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
[ehome@xman Demo8]$ sh Demo8-1.0.1-Linux.sh 
Demo8 Installer Version: 1.0.1, Copyright (c) Humanity
This is a self-extracting archive.
The archive will be extracted to: /home/ehome/Documents/programming/C/power/Demo8

If you want to stop extracting, please press <ctrl-C>.
The MIT License (MIT)

Copyright (c) 2013 Joseph Pan(http://hahack.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Do you accept the license? [yN]:
y
By default the Demo8 will be installed in:
"/home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux"
Do you want to include the subdirectory Demo8-1.0.1-Linux?
Saying no will install in: "/home/ehome/Documents/programming/C/power/Demo8" [Yn]:
y

Using target directory: /home/ehome/Documents/programming/C/power/Demo8/Demo8-1.0.1-Linux
Extracting, please wait...

Unpacking finished successfully

完成后提示安装到了 Demo8-1.0.1-Linux 子目录中,我们可以进去执行该程序:

1
2
3
[ehome@xman Demo8]$ ./Demo8-1.0.1-Linux/bin/Demo 5 2
Now we use our own Math library.
5 ^ 2 is 25

关于 CPack 的更详细的用法可以通过 man 1 cpack 参考 CPack 的文档。

版本控制工具

Git

参考链接-1 参考链接-2

Git是一个分布式版本控制系统,广泛用于软件开发中以跟踪和管理代码的变更。以下是一个基本的Git教程,帮助你入门Git的使用。

安装Git

  1. Windows: 可以从Git官网下载Git Bash。
  2. macOS: 使用Homebrew安装:brew install git
  3. Linux: 使用包管理器安装,例如在Ubuntu上:sudo apt-get install git

基本配置

安装完成后,进行一些基本配置:

1
2
git config --global user.name "Your Name"
git config --global user.email "you@example.com"

创建仓库

  1. 初始化一个新的Git仓库:

    1
    git init
  2. 克隆一个现有的仓库:

    1
    git clone <repository-url>

基本操作

  1. 查看状态:

    1
    git status
  2. 添加文件到暂存区:

    1
    git add <file1> <file2>

    或者添加所有更改:

    1
    git add .
  3. 提交更改:

    1
    git commit -m "Commit message"
  4. 查看提交历史:

    1
    git log

分支管理

  1. 查看分支:

    1
    git branch
  2. 创建新分支:

    1
    git branch <new-branch-name>
  3. 切换分支:

    1
    git checkout <branch-name>
  4. 创建并切换到新分支:

    1
    git checkout -b <new-branch-name>
  5. 合并分支:

    切换到目标分支(如main),然后合并:

    1
    2
    git checkout main
    git merge <branch-name>

远程操作

  1. 查看远程仓库:

    1
    git remote -v
  2. 添加远程仓库:

    1
    git remote add origin <repository-url>
  3. 推送到远程仓库:

    1
    git push origin <branch-name>
  4. 拉取远程更改:

    1
    git pull origin <branch-name>

撤销更改

  1. 撤销工作区中的更改:

    1
    git checkout -- <file>
  2. 撤销已暂存的更改:

    1
    git reset HEAD <file>
  3. 回滚到之前的提交:

    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.
Comments