Makefile学习2-----变量
变量的类别有递归扩展变量和简单扩展变量。只用一个“=”符号定义的变量被称为递归扩展变量(recursively expanded variable)。通过下面例子观察递归扩展变量的特点。
.PHONY: all
foo=$(bar)
bar=$(ugh)
ugh=Huh?
all:
@echo $(foo)

从结果来看,递归扩展变量的引用是递归的。
|
1
2
3
|
CFLAGS = $(include_dirs) -Oinclude_dirs = -Ifoo -Ibar |
上例所示的Makefile,CFLAGS变量最后都会被展开为“-Ifoo -Ibar -O”
|
1
|
CFLAGS =$(CFLAGS) -O |
上面的赋值代码将会造成一个死循环,无限递归。
简单扩展变量(simply expanded variable)使用“ :=”操作符来定义的。对于这种变量,make只对其进行一次展开,通过下面的代码来帮助我们理解:
.PHONY: all
x=foo
y=$(x) b
x=later
xx:=foo
yy:=$(xx) b
xx:=later
all:
@echo "X=$(y), xx=$(yy)"

递归和简单扩展变量相比的差距应该看出来了吧。递归相当于c++中的引用,而简单扩展变量make只对其进行一次展开。
下面对于同一个变量采取不同的赋值操作,看看会有什么效果。
.PHONY: all
objs=main.o foo.o bar.o utils.o
objs:=$(objs) another.o
all:
@echo $(objs)

如果把第二个简单扩展变量变成递归的即
objs=$(objs) another.o
make会报错

makefile:4:***递归变量'objs'引用本身(最终)。看来想引用自身的递归变量,编译器不会允许这样的行为。
在Makefile中还可以实现条件赋值:当变量没有被定义时就定义它,并且将右边的值赋值给它,如果变量已经定义了,则不改变其原值。条件赋值可用于为变量赋默认值。条件赋值运用条件赋值符“?=”来实现。下面用条件赋值运行一个Makefile:
.PHONY: all
foo=x
foo?=y
bar?=y
all:
@echo "foo=$(foo),bar=$(bar)"

foo已经定义,故保持原样,bar未定义,故把右侧数据赋值给bar。
另外一个非常有用的赋值方法是通过“+=”实现追加赋值:
.PHONY: all
objs=main.o foo.o bar.o utils.o
objs +=another.o
all:
@echo $(objs)

变量及其值的来源
从前面的示例可以看出,在Makefile中可以对变量进行定义。此外,还有其他的方式让make获得变量。比如:
(1)对于自动变量,其值是在每一个规则中根据上下文自动获得的
(2) 在运行make时,通过命令参数定义变量。如果以make bar=x的形式运行它,得到的结果则完全不同。从结果可以看出,在运行make的命令参数中定义的变量在Makefile中是可见的。其实完全可以通过make命令行中定义变量的方式覆盖Makefile文件中所定义变量的值。

(3)变量还可以来自shell环境,采用shell中的export命令定义一个bar变量之后运行Makefile。
export命令:设置或显示环境变量,具体用法请查看linux命令手册。

高级变量引用功能:
下面示例说明了变量引用的一种高级功能,即在赋值的同时完成文件名后缀替换操作
.PHONY: all
foo = a.c b.c c.c
bar :=$(foo:.c=.o)
all:
@echo "bar= $(bar)"

但是需要注意,(foo:.c=.o)中,foo:.c在":"和"."之间不能有空格,有空格将使后缀替换失败,(foo:.c=.o),相当于把foo变量后缀为.c的全部替换成.o的。
.PHONY: all
foo = a.c b.c c.c d.d
bar :=$(foo:.c=.o)
all:
@echo "bar= $(bar)"

可以看到,d.d并没被替换,只是替换后缀为.c的。
避免变量被覆盖的方法:
我们在设计Makefile时,可能并不希望发生变量被覆盖的现象,此时需要使用override指令进行限制。
.PHONY: all
override foo=x
foo=y
all:
@echo "foo= $(foo)"

借助“模式”精简规则:
对于目前simple项目的Makefile,其中存在多个规则用于构建目标文件。 比如main.o 和foo.o,都是采用不同的规则进行描述的。如果对于每一个目标文件,都得写一个不同的规则来描述,那真是一种体力活。Makefile中的模式就是用来解决这个问题的。
.PHONY: clean
CC = gcc
RM = rm
EXE =simple
OBJS =main.o foo.o
$(EXE): $(OBJS)
$(CC) -o $@ $^
#----------------------------------
#main.o:main.c\
$(CC) -o $@ -c $^\
foo.o:foo.c\
$(CC) -o $@ -c $^ 被屏蔽的代码段
#----------------------------------
%.o : %.c
$(CC) -o $@ -c $^
clean:
$(RM) -rf $(EXE) $(OBJS)

与前一个版本的相比,最为直观的改变就是将两条用于构建目标文件的规则变成了一条。模式类似与Windows操作系统中所使用的通配符,用“%”加以表示。采用了模式之后,无论有多少个源文件要编译都可以使用同一条规则,这极大地简化了Makefile。
同样,"%" 和"."之间不能有空格。还有一点,上面的代码使用了Makefile中的#加上反斜杠\的注释方式,是为了凸显和之前版本Makefile的差别。
通过函数能显著增强Makefile的功能。对于simple项目的Makefile,尽管使用了模式规则,但还是有一件比较麻烦的事情,就是要在Makefile中指明每一个项目源文件。下面介绍几个后期会使用到的函数,更多请参考《GUN Make》。
1.abspath函数
从命名就应该能够猜出它的作用。abspath函数用于将_name中的各路径名转化成绝对路径,并将转化后的结果返回。调用形式为:
$(abspath _name)
|
1
2
3
4
|
.PHONY: all root :=$(abspath /uer/../lib) all: @echo $(root) |

2.addprefix函数
addprefix函数用于给名字列表_names中的每一个名字增加前缀_prefix,并将增加了前缀的名字列表返回,调用形式为:
$(addprefix _prefix, _names)
|
1
2
3
4
5
|
.PHONY: allwithout_dir=main.c foo.cwith_dir :=$(addprefix objs/,$(without_dir))all: @echo $(with_dir) |

3.addsuffix函数
和前面addprefix刚好相反,addsuffix函数用于给名字列表_names中的每一个名字增加后缀_suffix,调用形式为:
$(addsuffix _suffix, _names)
|
1
2
3
4
5
|
.PHONY: allwithout_dir=main foowith_dir :=$(addsuffix .c,$(without_dir))all: @echo $(with_dir) |

4.filter函数
filter函数被用于从一个名字列表_text中根据模式_patterm得到满足需要的名字列表并返回,其形式是:
$(filter _pattern,_text)
|
1
2
3
4
|
.PHONY: allsources =foo.o bar.c main.c hell.ssources :=$(filter %.c %.s,$(sources))all: @echo $(sources) |

从结果来看,调用filter函数后source变量中只存在.c文件和.s文件,而.o文件因为不满足所指定的模式而被过滤掉了。
5.eval函数
eval函数的存在使得Makefile具有动态语言的特征。eval函数使得make将再一次解析_text语句。eval返回空字符串,调用形式为:
$(eval _text)
|
1
2
3
4
5
|
.PHONY: allsources =foo.o bar.c main.c hell.s$(eval sources :=$(filter %.c %.s,$(sources)))all: @echo $(sources) |

虽然它和上面第四个函数运行结果完全一样,但是在某些场合却必须用eval。
在 Makefile 中,eval 函数用于动态地生成和执行 Makefile 语句。它可以将字符串解析为有效的 Makefile 代码并立即执行。以下是 eval 的用法和注意事项:
基本语法
$(eval <expression>)
<expression> 是一个字符串,通常包含 Makefile 的变量定义、规则或其他语句。
使用场景
(1).动态生成变量
VAR_NAME = my_var
$(eval $(VAR_NAME) = value)
结果:my_var = value
(2).动态生成规则
TARGET = output
$(eval $(TARGET): ; @echo "Building $(TARGET)")
结果:生成一个名为 output 的目标。
(3).循环生成规则或变量
FILES = file1 file2 file3
$(foreach file, $(FILES), $(eval $(file)_size = $(shell wc -c < $(file))))
结果:为每个文件生成一个变量 <file>_size,存储文件大小。
注意事项
(1).避免语法错误
eval 会直接执行生成的代码,确保生成的代码是有效的 Makefile 语句。
使用 $(info ...) 调试生成的代码:
$(info $(eval VAR = value))
(2).变量展开
eval 会在执行时展开变量,因此需要注意变量的引用方式:
使用 $() 引用变量。
如果需要延迟展开,使用 $$。
(3).调试困难
如果 eval 生成的代码有问题,Makefile 的错误信息可能不直观。可以通过打印生成的代码来调试:
$(info $(eval ...))
(4).性能问题
eval 会增加 Makefile 的解析复杂度,尽量避免过度使用。
(5).作用域
eval 生成的变量或规则在当前作用域内有效。如果需要全局作用域,确保在顶层定义。
示例:动态生成规则
FILES = file1 file2 file3
$(foreach file, $(FILES), \
$(eval $(file): ; @echo "Processing $(file)"))
运行 make file1 时,会输出:
Processing file1
通过合理使用 eval,可以动态生成复杂的规则和变量,但需要注意调试和代码可读性。
eval的二次展开,是递归的一种形式,因为有时候在Makefile的表达式中,最后得出来的可能还是Makefile的表达式而非真正我们想要传递的值,需要再展开Makefile的表达式得到最终的结果。
6.filter-out函数
该函数用于从名字列表_text中根据模式_pattern滤除一部分名字并将滤除后的列表返回,其形式为:
$(filter-out _pattern,_text)
|
1
2
3
4
5
|
.PHONY: allobjs =foo.o main.o main1.o main2.oresult :=$(filter-out main%.o,$(objs))all: @echo $(result) |

7.notdir函数
该函数用来从路径_name中抽取文件名,并将文件名返回。其形式为:
$(notdir _name)
|
1
2
3
4
|
.PHONY: allfile_name :=$(notdir c/d/e/f/a.c q/w/e/r/b.c)all: @echo $(file_name) |

8.patsubst函数
该函数用来将名字列表_text中符合_pattern模式的名字替换成_replacement,并将替换后的名字列表返回。其形式为:
$(patsubst _pattern,_replacement,_text)
|
1
2
3
4
5
|
.PHONY: allmixed=foo.c bar.c main.oobjs :=$(patsubst %.c,%.o,$(mixed))all: @echo $(objs) |

9.realpath函数
该函数用于获取_name所对应的真实路径名。其形式为:
$(realpath _name)
|
1
2
3
4
|
.PHONY: allroot :=$(realpath ./)all: @echo $(root) |

10.strip函数
如果希望清除名字列表中的多余空格,strip函数是最佳选择,它将_string中的多余空格去除后返回。其形式为:
$(strip _string)
.PHONY: all
ori=foo.c main.c
res:=$(strip $(ori))
all:
@echo "$(ori)"
@echo "$(res)"

这里对echo命令做了一点变动,细心的人已经发现加了一个双引号,如果不加双引号,这里的两个输出将是相同的,都是输出foo.c main.c。
双引号用于保持引号内所有字符的字面值(回车和空格也不例外)。
11.wildcard函数
该函数是通配符函数,通过它可以得到当前工作目录中满足_pattern模式的文件或目录名列表。其形式为:
$(wildcard _pattern)
|
1
2
3
4
|
.PHONY:allsrcs=$(wildcard *.c)all: @echo $(srcs) |

有了上面函数的基础之后,我们来看看之前的simple项目,对它进行改进:
之前的代码:
.PHONY: clean
CC = gcc
RM = rm
EXE =simple
OBJS =main.o foo.o
$(EXE): $(OBJS)
$(CC) -o $@ $^
%.o : %.c
$(CC) -o $@ -c $^
clean:
$(RM) -rf $(EXE) $(OBJS)
还是要手动添加源文件,麻烦,运用函数之后,改进如下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
.PHONY: cleanCC = gccRM = rmEXE =simpleSRCS =$(wildcard *.c)OBJS =$(patsubst %.c, %.o, $(SRCS))$(EXE): $(OBJS) $(CC) -o $@ $^%.o : %.c $(CC) -o $@ -c $^clean: $(RM) -rf $(EXE) $(OBJS) |

此时我们增加一个源文件,touch bar.c,看看我们的Makefile需不需要修改。

从结果看,不用修改Makefile就可以增加源文件了,同理,删除也一样,这样的Makefile具有更好的鲁棒性。
在大多项目中都会合理设计目录结构来提高维护性,在编译一个项目时会产生大量中间文件,如果中间文件直接和源文件放在一起,就显得杂乱而不利于维护。在为现在这个complicated项目编写makefile之前,我们先给出目录结构需求:
1.将所有的目标文件放在objs子目录中;
2.将最终生成的可执行程序放在exes子目录中;
在编译项目之前,需要将生成的文件目录准备好,可以手动创建,也可以通过编译Makefile创建。
.PHONY: all clean
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
DIRS =objs exes
all:$(DIRS)
$(DIRS):
$(MKDIR) $@
clean:
$(RM) $(RMFLAGS) $(DIRS)
为了将项目编译时所创建的文件分别放入objs和exes中,需要用到前面介绍的一个函数,addprefix。
.PHONY: all clean
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
CC=gcc
DIR_OBJS=objs
DIR_EXES=exes
DIRS =$(DIR_OBJS) $(DIR_EXES)
EXE=complicated
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/,$(OBJS))
all:$(DIRS) $(DIR_EXES)/$(EXE)
$(DIRS):
$(MKDIR) $@
$(DIR_EXES)/$(EXE):$(OBJS)
$(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c
$(CC) -o $@ -c $^
clean:
$(RM) $(RMFLAGS) $(DIRS) $(EXE)
这里运行之后就会在objs文件夹中存放.o,在exes文件中存放可执行文件complicated。由于书上是把可执行文件放在当前目录的,所以它在clean加上了删除$(EXE),在我的Makefile中其实这句多余,因为把exes文件夹都删除了,在单独删除exes文件中的可执行文件没有意义,之所以没改,是为了提醒一下自己,这里完成了书上的不再赘述的任务O(∩_∩)O。
到这里 ,你可能觉得自己又get了一个新技能,但是,这却不是一个好的Makefile,比如,我们把foo.h更改为:
#ifndef __FOO_H #define __FOO_H void foo(int value); #endif /*__FOO_H*/
这里仅仅改变了foo函数的参数,从void变成了int,但是我们执行make:

注意,这里是在没有更改foo.h之前先make一次,然后在更改了foo.h之后再make,然后make提示居然没有任何事情可以做?奇怪吧,我们明明更改了函数声明不再匹配了啊,我们执行clean之后,再执行make:

这下make终于发现错误了,为什么第一次重新make的时候,make不发现错误呢?因为我们的Makefile中,并没有依赖foo.h。
在改进之前,我们分析一下make为什么不能发现foo.h的更改并对项目进行重新编译。下图是现有的Makefile所表达的依赖关系树。从图中我们并不能找到foo.h文件的身影,也就是说,从make的角度来看它并不知道foo.h的存在,因而也不能侦测到foo.h文件的变动并对项目进行重新编译。

所以,我们可以更改Makefile如下:
.PHONY: all clean
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
CC=gcc
DIR_OBJS=objs
DIR_EXES=exes
DIRS =$(DIR_OBJS) $(DIR_EXES)
EXE=complicated
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/,$(OBJS))
all:$(DIRS) $(DIR_EXES)/$(EXE)
$(DIRS):
$(MKDIR) $@
$(DIR_EXES)/$(EXE):$(OBJS)
$(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c foo.h
$(CC) -o $@ -c $<
clean:
$(RM) $(RMFLAGS) $(DIRS) $(EXE)
其中红色部分为更改部分。

其中的改动非常小,即将foo.h文件作为每一个.o文件的先决条件。在这个Makefile中,首次使用了自动变量$<。
用$<的目的是为了只将.c文件作为gcc的输入内容。
可以看到,我们先执行make生成目标文件,然后更改foo.h,再make,立即报错了。虽然这样可以解决问题,但是当项目复杂时,如果每一个头文件都要写入Makefile相应的规则中,那么将会是一个噩梦。
------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
后面会介绍gcc获得源文件依赖的方法,gcc这个功能就是为make而存在的。我们采用gcc的-MM选项结合sed命令。使用sed进行替换的目的是为了在目标名前加上“objs/”前缀。gcc的-E选项,预处理。在生成依赖关系时,其实并不需要gcc编译源文件,只要预处理就可以获得依赖关系了。通过-E选项,可以避免生成依赖关系时gcc发出警告,以及提高依赖关系的生成效率。
现在,已经找到自动生成依赖关系的方法了,那么如何将其整合到我们complicated项目的Makefile中呢?自动生成的依赖信息不能直接出现在Makefile中,因为不能动态地改变Makefile中的内容,此时我们需要通过创建依赖关系文件的方式。假设依赖关系的文件以“.dep”结尾,因此我们新创建一个deps文件,用来存放依赖关系文件信息。
Makefile如下:
.PHONY: all clean
MKDIR = mkdir
RM = rm
RMFLAGS = -rf
CC=gcc
DIR_OBJS=objs
DIR_EXES=exes
DIR_DEPS=deps
DIRS =$(DIR_OBJS) $(DIR_EXES) $(DIR_DEPS)
EXE=complicated
EXE:=$(addprefix $(DIR_EXES)/,$(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/,$(OBJS))
DEPS=$(SRCS:.c=.dep)
DEPS:=$(addprefix $(DIR_DEPS)/,$(DEPS))
all:$(DIRS) $(DEPS) $(EXE)
$(DIRS):
$(MKDIR) $@
$(EXE):$(OBJS)
$(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c
$(CC) -o $@ -c $^
$(DIR_DEPS)/%.dep:%.c
@echo "Creating $@ ..."
@set -e;\
$(RM) $(RMFLAGS) $@.tmp;\
$(CC) -E -MM $^ >$@.tmp;\
sed 's,\(.*\)\.o[:]*,objs/\1.o:,g' <$@.tmp >$@;\
$(RM) $(RMFLAGS) $@.tmp
clean:
$(RM) $(RMFLAGS) $(DIRS)
和之前的complicated项目的Makefile相比:
1,增加了deps文件夹。
2,删除了目标文件创建规则中的foo.h依赖,并将规则中的自动变量从$<变回了$^。
3,增加了DEPS变量用于存放依赖文件。
4,为all目标增加了对 $(DEPS) 的依赖。
5,增加了一个用于创建依赖关系文件的规则。在这个规则中,使用了gcc的-E和-MM选项来获取依赖关系。在生成最终的依赖关系文件之前,使用了一个由$@.tmp表示的临时文件,且在依赖文件生成以后将其删除。“set -e” 的作用是告诉shell,在生成依赖关系文件的过程中如果出现任何错误就直接退出。shell异常退出的最终表现就是make会告诉我们出错了,从而停止后续的make工作。如果不设置这一行,当构建依赖出错时,make还会继续后面的工作(并最终出错),这并不是我们希望看到的。读者可以测试故意在源文件或者头文件中植入错误并去掉 “set -e” 选项观察make的行为和加上set -e有何不同。
这里还有几个知识点需要补充。
1.对于规则中的每一条命令,make都是在一个新的shell上运行它的。
2.如果希望多个命令在同一个shell中运行,可以用“;”将这些命令连起来。
3.当命令很长时,可以用“\”将一个命令书写成多行。
为了更好的理解第一点,我们做一个实验。现假设需要创建一个test目录,然后在这个test目录下再创建一个subtest子目录。编写Makefile如下:
.PHONY:all
all:
@mkdir test
@cd test
@mkdir subtest

可以看到test和subtest是同级目录并非父子目录,然后用上面提到的知识点更改Makefile:
.PHONY:all
all:
@mkdir test;\
cd test;\
mkdir subtest

这样就可以达到目的了。不过你可能会想,为什么这里后面的cd和最后一个mkdir不需要在前面加上@呢?那么我们加上试试呢?

如果使用了分号“ ;”,表示命令在同一个shell中运行,而且使用“ \”链接一条命令,既然是一条命令,自然不能够识别后面的@cd或者@mkdir,因为最开始的mkdir使用@,让终端不显示执行的指令,后面的cd和mkdir是在前面操作的情况下进行的 ,此时,直接使用命令即可。
还有一个需要注意的地方:
如同
EXE=complicated
EXE:=$(addprefix $(DIR_EXES)/,$(EXE))
这样的Makefile,为什么第二个赋值我们是用:=而不是直接=呢?这也是需要注意的小细节,这个在之前的随笔中已经说过,要是用=,会导致无限递归,为什么呢?因为EXE在复制号左边,而右边又有$(EXE),这样会无限调用,make报错。不信你可以试试。
sed 's,\(.*\)\.o[:]*,objs/\1.o:,g' <$@.tmp >$@;
3.命令与选项
sed命令告诉sed如何处理由地址指定的各输入行,如果没有指定地址则处理所有的输入行。
此处sed引用本人的另一篇博客, 参考链接:sed命令详解 - blogernice - 博客园
3.1 sed命令
| 命令 | 功能 |
| a\ |
在当前行后添加一行或多行。多行时除最后一行外,每行末尾需用“\”续行 |
| c\ | 用此符号后的新文本替换当前行中的文本。多行时除最后一行外,每行末尾需用"\"续行 |
| i\ | 在当前行之前插入文本。多行时除最后一行外,每行末尾需用"\"续行 |
| d | 删除行 |
| h | 把模式空间里的内容复制到暂存缓冲区 |
| H | 把模式空间里的内容追加到暂存缓冲区 |
| g | 把暂存缓冲区里的内容复制到模式空间,覆盖原有的内容 |
| G | 把暂存缓冲区的内容追加到模式空间里,追加在原有内容的后面 |
| l | 列出非打印字符 |
| p | 打印行 |
| n | 读入下一输入行,并从下一条命令而不是第一条命令开始对其的处理 |
| q | 结束或退出sed |
| r | 从文件中读取输入行 |
| ! | 对所选行以外的所有行应用命令 |
| s | 用一个字符串替换另一个 |
| g | 在行内进行全局替换 |
| w | 将所选的行写入文件 |
| x | 交换暂存缓冲区与模式空间的内容 |
| y | 将字符替换为另一字符(不能对正则表达式使用y命令) |
3.2 sed选项
| 选项 | 功能 |
| -e | 进行多项编辑,即对输入行应用多条sed命令时使用 |
| -n | 取消默认的输出 |
| -f | 指定sed脚本的文件名 |
4.退出状态
5.正则表达式元字符
| 元字符 | 功能 | 示例 |
| ^ | 行首定位符 | /^my/ 匹配所有以my开头的行 |
| $ | 行尾定位符 | /my$/ 匹配所有以my结尾的行 |
| . | 匹配除换行符以外的单个字符 | /m..y/ 匹配包含字母m,后跟两个任意字符,再跟字母y的行 |
| * | 匹配零个或多个前导字符 | /my*/ 匹配包含字母m,后跟零个或多个y字母的行 |
| [] | 匹配指定字符组内的任一字符 | /[Mm]y/ 匹配包含My或my的行 |
| [^] | 匹配不在指定字符组内的任一字符 | /[^Mm]y/ 匹配包含y,但y之前的那个字符不是M或m的行 |
| \(..\) | 保存已匹配的字符 | 1,20s/\(you\)self/\1r/ 标记元字符之间的模式,并将其保存为标签1,之后可以使用\1来引用它。最多可以定义9个标签,从左边开始编号,最左边的是第一个。此例中,对第1到第20行进行处理,you被保存为标签1,如果发现youself,则替换为your。 |
| & | 保存查找串以便在替换串中引用 | s/my/**&**/ 符号&代表查找串。my将被替换为**my** |
| \< | 词首定位符 | /\<my/ 匹配包含以my开头的单词的行 |
| \> | 词尾定位符 | /my\>/ 匹配包含以my结尾的单词的行 |
| x\{m\} | 连续m个x | /9\{5\}/ 匹配包含连续5个9的行 |
| x\{m,\} | 至少m个x | /9\{5,\}/ 匹配包含至少连续5个9的行 |
| x\{m,n\} | 至少m个,但不超过n个x | /9\{5,7\}/ 匹配包含连续5到7个9的行 |
并不是只有 / 可作为模式分割符,很多符合如 , ; 都可以,尤其是模式中有 / 时使用其他分割符更方便,这里这个复杂例子使用逗号,做模式分隔符;
sed 's,\(.*\)\.o[:]*,objs/\1.o:,g' <$@.tmp >$@;
现在来分解这个复杂表达式,首先,sed s表示我们想用一个字符串替换另一个字符串,这也是我们使用sed的原因,它的s命令就
可以达到这个效果。
's,\(.*\)\.o[:]*,objs/\1.o:,g'第一次分解,此时需要知道,单引号是一对的,即s前面的'和g后面的'是一个整体单引号,
这也是sed命令的基础,至于单引号和双引号有什么区别,可百度谷歌或者必应。(但是我之前测试的单引号和双引号并不是我搜索所显示的那样,后面再试试吧)
继续分解,s,中s是替换字符串的意思,这个在上面的表格中可以查询到,逗号,表示模式分隔符,在这种有/出现的字符串中,我们选择了逗号,作为分隔符号。
所以下一次分解应该倒下一个逗号处,
\(.*\)\.o[:]*,
这里首先看 .* 它表示匹配任意字符,\( \)是一个整体,也是通过上面的表格得到的,然后转义字符\和.o在一起,把.的作用(匹配除换行符的单个字符)变成普通的.(就是一个字符.),那么这一句话就是
操作字符串所有有.o的且在.o后面(可以有空格)匹配:的零个或多个字符串。
objs/\1.o:,g
这里要解释的是\1.o 这里用了转义字符\加上1,这表示什么呢?尤其是这个1,表示的就是前面第一个字符串,这是标签
的概念,如何知道是第几标签呢?前面第一个用\( \)这个括起来的就是第一个标签,用转义字符\1表示,依次类推。g在sed中表示行内全局替换
这样,我们做一个假设例子来说明。
abc.o : 用这个代表\(.*\)\.o[:]*
然后objs/\1.o:,g之后呢,abc.o :变成了 objs/abc.o: 这里相当于给前面加上了objs/前缀,并且把.o和:之前的空格去掉了
最后这个<$@.tmp >$@;这不属于sed的内容了,属于linux和Makefile的东西,$@.tmp重定向输入给前面的sed替换操作,
$@代表目标在Makefile中,$@.tmp是前面的Makefile生成的,<重定向,看方向是输入,
就是把$@.tmp重定向输入给sed,经过sed替换之后,再输出重定向 > 到$@,这个是目标。
这样再回过头去看之前那个Makefile就可以看懂了。
posted on 2019-12-05 20:06 blogernice 阅读(693) 评论(0) 收藏 举报


浙公网安备 33010602011771号