blogernice

导航

Makefile学习2-----变量

变量的类别有递归扩展变量和简单扩展变量。只用一个“=”符号定义的变量被称为递归扩展变量(recursively expanded variable)。通过下面例子观察递归扩展变量的特点。

复制代码
.PHONY: all

foo=$(bar)
bar=$(ugh)
ugh=Huh?

all:
    @echo $(foo)
复制代码

 

从结果来看,递归扩展变量的引用是递归的。

 

1
2
3
CFLAGS = $(include_dirs) -O
 
include_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: all
without_dir=main.c foo.c
with_dir :=$(addprefix objs/,$(without_dir))
all:
    @echo $(with_dir)

 

3.addsuffix函数

和前面addprefix刚好相反,addsuffix函数用于给名字列表_names中的每一个名字增加后缀_suffix,调用形式为:

$(addsuffix _suffix, _names)

1
2
3
4
5
.PHONY: all
without_dir=main foo
with_dir :=$(addsuffix .c,$(without_dir))
all:
    @echo $(with_dir)

 

4.filter函数

filter函数被用于从一个名字列表_text中根据模式_patterm得到满足需要的名字列表并返回,其形式是:

$(filter _pattern,_text) 

1
2
3
4
.PHONY: all
sources =foo.o bar.c main.c hell.s
sources :=$(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: all
sources =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: all
objs =foo.o main.o main1.o main2.o
result :=$(filter-out main%.o,$(objs))
all:
    @echo $(result)

 

7.notdir函数

该函数用来从路径_name中抽取文件名,并将文件名返回。其形式为:

$(notdir _name)

1
2
3
4
.PHONY: all
file_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: all
mixed=foo.c bar.c main.o
objs :=$(patsubst %.c,%.o,$(mixed))
all:
    @echo $(objs)

9.realpath函数

该函数用于获取_name所对应的真实路径名。其形式为:

$(realpath _name)

1
2
3
4
.PHONY: all
root :=$(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:all
srcs=$(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: clean
 
CC = gcc
RM = rm
 
EXE =simple
SRCS =$(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 >$@;
这个语句才是最难的,也是最费力的。
首先要简单说明一下linux中sed的用法:
1.简介
sed是非交互式的编辑器。它不会修改文件,除非使用shell重定向来保存结果(这里这个复杂命令就用了重定向来更改)。默认情况下,所有的输出行都被打印到屏幕上。
sed编辑器逐行处理文件(或输入),并将结果发送到屏幕。具体过程如下:首先sed把当前正在处理的行保存在一个临时缓存区中(也称为模式空间),然后处理临时缓冲区中的行,完成后把该行发送到屏幕上。sed每处理完一行就将其从临时缓冲区删除,然后将下一行读入,进行处理和显示。处理完输入文件的最后一行后,sed便结束运行。sed把每一行都存在临时缓冲区中,对这个副本进行编辑,所以不会修改原文件。
2.定址
定址用于决定对哪些行进行编辑。地址的形式可以是数字、正则表达式、或二者的结合。如果没有指定地址,sed将处理输入文件的所有行

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.退出状态

sed不向grep一样,不管是否找到指定的模式,它的退出状态都是0。只有当命令存在语法错误时,sed的退出状态才不是0。

 

5.正则表达式元字符

 与grep一样,sed也支持特殊元字符,来进行模式查找、替换。不同的是,sed使用的正则表达式是括在斜杠线"/"之间的模式。
如果要把正则表达式分隔符"/"改为另一个字符,比如o,只要在这个字符前加一个反斜线,在字符后跟上正则表达式,再跟上这个字符即可。例如:sed -n '\o^Myop' datafile

 

 元字符  功能  示例
 ^  行首定位符  /^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)    收藏  举报