nginx笔记
nginx deny all
仅允许某个网段访问,其他全部拒绝
尽管允许某个网段访问,依然无法访问,原因是/error403.html页面也被deny了,正确的如下
|
|
如果想配置多个错误页
|
|
nginx 400 bad request 客户端发送cookies过大
调整large_client_header_buffers大小
多条件rewrite
nginx 只有if指令没有else指令, 如果存在多重条件,可以通过多个变量实现
仅允许某个网段访问,其他全部拒绝
尽管允许某个网段访问,依然无法访问,原因是/error403.html页面也被deny了,正确的如下
|
|
如果想配置多个错误页
|
|
调整large_client_header_buffers大小
nginx 只有if指令没有else指令, 如果存在多重条件,可以通过多个变量实现
http://zeromq.org/bindings:php
需要libzmq-dev包
|
|
|
|
|
|
|
|
|
|
速度最快的消息中间件,支持各种客户端,缺点是不能保证消息丢失
从AMQP协议可以看出,MessageQueue、Exchange和Binding构成了AMQP协议的核心,下面我们就围绕这三个主要组件 从应用使用的角度全面的介绍如何利用Rabbit MQ构建消息队列以及使用过程中的注意事项。
在Rabbit MQ中,无论是生产者发送消息还是消费者接受消息,都首先需要声明一个MessageQueue。这就存在一个问题,是生产者声明还是消费者声明呢?要解决这个问题,首先需要明确:
a)消费者是无法订阅或者获取不存在的MessageQueue中信息。
b)消息被Exchange接受以后,如果没有匹配的Queue,则会被丢弃。
在明白了上述两点以后,就容易理解如果是消费者去声明Queue,就有可能会出现在声明Queue之 前,生产者已发送的消息被丢弃的隐患。如果应用能够通过消息重发的机制允许消息丢失,则使用此方案没有任何问题。但是如果不能接受该方案,这就需要无论是 生产者还是消费者,在发送或者接受消息前,都需要去尝试建立消息队列。这里有一点需要明确,如果客户端尝试建立一个已经存在的消息队列,Rabbit MQ不会做任何事情,并返回客户端建立成功的。
如果一个消费者在一个信道中正在监听某一个队列的消息,Rabbit MQ是不允许该消费者在同一个channel去声明其他队列的。Rabbit MQ中,可以通过queue.declare命令声明一个队列,可以设置该队列以下属性:
a) Exclusive: 排他队列,如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:其一,排他队列是基于连接可见 的,同一连接的不同信道是可以同时访问同一个连接创建的排他队列的。其二,“首次”,如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排 他队列的,这个与普通队列不同。其三,即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除的。这种队列适用于只限于一个客户端 发送读取消息的应用场景。
b) Auto-delete:自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。这种队列适用于临时队列。
c) Durable:持久化,这个会在后面作为专门一个章节讨论。
d) 其他选项,例如如果用户仅仅想查询某一个队列是否已存在,如果不存在,不想建立该队列,仍然可以调用queue.declare,只不过需要将参数passive设为true,传给queue.declare,如果该队列已存在,则会返回true;如果不存在,则会返回Error,但是不会创建新的队列。
在AMQP模型中,Exchange是接受生产者消息并将消息路由到消息队列的关键组件。ExchangeType和Binding决定了消息的路由规 则。所以生产者想要发送消息,首先必须要声明一个Exchange和该Exchange对应的Binding。可以通过 ExchangeDeclare和BindingDeclare完成。在Rabbit MQ中,声明一个Exchange需要三个参数:ExchangeName,ExchangeType和Durable。ExchangeName是该 Exchange的名字,该属性在创建Binding和生产者通过publish推送消息时需要指定。ExchangeType,指Exchange的类 型,在RabbitMQ中,有三种类型的Exchange:direct ,fanout和topic,不同的Exchange会表现出不同路由行为。Durable是该Exchange的持久化属性,这个会在消息持久化章节讨 论。声明一个Binding需要提供一个QueueName,ExchangeName和BindingKey。下面我们就分析一下不同的 ExchangeType表现出的不同路由规则。
生产者在发送消息时,都需要指定一个RoutingKey和Exchange,Exchange在接到该RoutingKey以后,会判断该ExchangeType:
a) 如果是Direct类型,则会将消息中的RoutingKey与该Exchange关联的所有Binding中的BindingKey进行比较,如果相等,则发送到该Binding对应的Queue中。
b) 如果是Fanout类型,则会将消息发送给所有与该Exchange定义过Binding的所有Queues中去,其实是一种广播行为。
c)如果是Topic类型,则会按照正则表达式,对RoutingKey与BindingKey进行匹配,如果匹配成功,则发送到对应的Queue中。
在RabbitMQ中消费者有2种方式获取队列中的消息:
a) 一种是通过basic.consume命令,订阅某一个队列中的消息,channel会自动在处理完上一条消息之后,接收下一条消息。(同一个channel消息处理是串行的)。除非关闭channel或者取消订阅,否则客户端将会一直接收队列的消息。
b) 另外一种方式是通过basic.get命令主动获取队列中的消息,但是绝对不可以通过循环调用basic.get来代替basic.consume,这是因为basic.get RabbitMQ在实际执行的时候,是首先consume某一个队列,然后检索第一条消息,然后再取消订阅。如果是高吞吐率的消费者,最好还是建议使用basic.consume。
如果有多个消费者同时订阅同一个队列的话,RabbitMQ是采用循环的方式分发消息的,每一条消息只能被一个订阅者接收。例如,有队列Queue,其中ClientA和ClientB都Consume了该队列,MessageA到达队列后,被分派到ClientA,ClientA回复服务器收到响应,服务器删除MessageA;再有一条消息MessageB抵达队列,服务器根据“循环推送”原则,将消息会发给ClientB,然后收到ClientB的确认后,删除MessageB;等到再下一条消息时,服务器会再将消息发送给ClientA。
这里我们可以看出,消费者再接到消息以后,都需要给服务器发送一条确认命令,这个即可以在handleDelivery里显示的调用basic.ack实现,也可以在Consume某个队列的时候,设置autoACK属性为true实现。这个ACK仅仅是通知服务器可以安全的删除该消息,而不是通知生产者,与RPC不同。 如果消费者在接到消息以后还没来得及返回ACK就断开了连接,消息服务器会重传该消息给下一个订阅者,如果没有订阅者就会存储该消息。
既然RabbitMQ提供了ACK某一个消息的命令,当然也提供了Reject某一个消息的命令。当客户端发生错误,调用basic.reject命令拒绝某一个消息时,可以设置一个requeue的属性,如果为true,则消息服务器会重传该消息给下一个订阅者;如果为false,则会直接删除该消息。当然,也可以通过ack,让消息服务器直接删除该消息并且不会重传。
Rabbit MQ默认是不持久队列、Exchange、Binding以及队列中的消息的,这意味着一旦消息服务器重启,所有已声明的队列,Exchange,Binding以及队列中的消息都会丢失。通过设置Exchange和MessageQueue的durable属性为true,可以使得队列和Exchange持久化,但是这还不能使得队列中的消息持久化,这需要生产者在发送消息的时候,将delivery mode设置为2,只有这3个全部设置完成后,才能保证服务器重启不会对现有的队列造成影响。这里需要注意的是,只有durable为true的Exchange和durable为true的Queues才能绑定,否则在绑定时,RabbitMQ都会抛错的。持久化会对RabbitMQ的性能造成比较大的影响,可能会下降10倍不止。
对事务的支持是AMQP协议的一个重要特性。假设当生产者将一个持久化消息发送给服务器时,因为consume命令本身没有任何Response返回,所以即使服务器崩溃,没有持久化该消息,生产者也无法获知该消息已经丢失。如果此时使用事务,即通过txSelect()开启一个事务,然后发送消息给服务器,然后通过txCommit()提交该事务,即可以保证,如果txCommit()提交了,则该消息一定会持久化,如果txCommit()还未提交即服务器崩溃,则该消息不会服务器就收。当然Rabbit MQ也提供了txRollback()命令用于回滚某一个事务。
使用事务固然可以保证只有提交的事务,才会被服务器执行。但是这样同时也将客户端与消息服务器同步起来,这背离了消息队列解耦的本质。Rabbit MQ提供了一个更加轻量级的机制来保证生产者可以感知服务器消息是否已被路由到正确的队列中——Confirm。如果设置channel为confirm状态,则通过该channel发送的消息都会被分配一个唯一的ID,然后一旦该消息被正确的路由到匹配的队列中后,服务器会返回给生产者一个Confirm,该Confirm包含该消息的ID,这样生产者就会知道该消息已被正确分发。对于持久化消息,只有该消息被持久化后,才会返回Confirm。Confirm机制的最大优点在于异步,生产者在发送消息以后,即可继续执行其他任务。而服务器返回Confirm后,会触发生产者的回调函数,生产者在回调函数中处理Confirm信息。如果消息服务器发生异常,导致该消息丢失,会返回给生产者一个nack,表示消息已经丢失,这样生产者就可以通过重发消息,保证消息不丢失。Confirm机制在性能上要比事务优越很多。但是Confirm机制,无法进行回滚,就是一旦服务器崩溃,生产者无法得到Confirm信息,生产者其实本身也不知道该消息吃否已经被持久化,只有继续重发来保证消息不丢失,但是如果原先已经持久化的消息,并不会被回滚,这样队列中就会存在两条相同的消息,系统需要支持去重。
1.什么情况会导致 blackhole?
两种情况:
声明 exchange 后未绑定任何 queue ,此时发送到该 exchange 上的 message 均被 blackholed ;
声明 exchange 后绑定了 queue ,但发送到该 exchange 上的 message 所使用的 routing_key 不匹配任何 binding_key ,则 blackholed 。
本文转自说说常见搜索引擎的分布式解决方法
随着索引数据的增大以及请求的增多,分布式搜索是最好的一种解决方案,主要解决两个问题,其一是能让单台机器load所有索引数据到内存中,其二是请求延时大,解决请求latency问题。我之前为团队写了篇专利,内容是关于分布式搜索解决方案的,所以粗略的看了下大部分开源的搜索引擎是怎么实现分布式的,后面的文章我会简单说下常见的搜索引擎的分布式解决方案。
首先我们先说下几个简单概念,分布式搜索都是M*N(横向切分数据,纵向切分流量)这个维度去解决问题的,虽然不同产品或场景概念不完全相同,读者可以简单认为一份完整的数据,被均分为M份,每一份被称为一个分配(Shard或者Partition),然后提供对每个Shard提供N份副本(Replica)。那么分布式的设计就围绕着以下问题:
下面就说下常见的搜索引擎的分布式解决方案,因为开源的搜索产品基本上都没有在工作中用过,对代码细节并不是太了解,只是研究了下其原理,所以理解的会有些偏差,看官们如果发现错误直接指出即可。
Sphinx的流程还是很简单的,可以看下其流程图:
需要支持分布式的话,需要改下配置,大致是这样子的:
|
|
从图中也可以看出,需要在配置列表里配置好其他shard的地址。查询过程为:
主从同步
增量更新索引
方法也是设置crontab,添加2个选项,一个是重建主索引,一个是增量索引更新。
当然为了避免单点以及增加服务能力,肯定有多个Replica,解决方法应该也是配置或者haproxy相关的方法解决,从上面可以看出,Sphinx很难用,自动化能力太弱,所以很多大厂要么不再使用Sphinx要么基于其二次开发。
Solr提供了两种方案来应对访问压力,其一是Replication,另一个是SolrCloud。我们此处只说Replication原理。
Replication采用了Master/Slave模式,也就是说由一个主索引和多个从索引构成,从索引从主索引复制索引,主索引负责更新索引,从索引负责同步索引和查询。本质上是读写分离的思想,MySQL/Redis等数据库也多是这种方式部署的。有两种部署方式:
与第一种相比多了一层Repeater,Repeater既扮演了Master角色,又扮演了Slave功能,主要解决单个Master下Slave太多,Master压力太大的问题。
Master与Slave之间的通信是无状态的http连接,Slave端发送不同的Command从Master端获得数据。原理就是Master那边有个标志位和版本号,用于获取正确的数据版本,然后数据扔到Slave临时目录下,数据完整后,再覆盖原有数据。多个副本的方法应该与Sphinx相似,一般也是通过通过上游负载均衡模块如Nginx,HaProxy来分流。
因为Solr Replication不好用,本质上还不算真正分布式的,所以Solr从4.0开始支持SolrCloud模式。特性不少,主要说两个吧:
配置文件统一管理,扔到Zookeeper上
自动做负载均衡和故障恢复,不再需要Nginx或HaProxy的支持
逻辑图
任意节点收到创建索引请求后,转换成json格式存储到zk的/overseer/collection-queue-work的children节点上
leader线程一直监控collection-queue-work节点,检查到变化后,取出json数据,根据信息计算出需要创建的shard、replica,将创建具体replica的请求转向各对应节点
各节点创建完具体的replica后,将该节点的状态(创建成功与否等)更新到/overseer/queue的children节点上
leader线程监控/overseer/queue节点,将overseer/queue的children节点的状态更新至/clusterstate.json
各节点同步/clusterstate.json,整个集群状态得到更新,新索引创建成功
根据路由规则计算出该doc所属shard,并找出该doc所属的shard对应的leader
如果当前Replica是对应Shard且是leader,首先更新本地索引,然后再将doc转向该Shard的其余Replica
扩容/缩容
停掉某台Solr,更新集群状态到/clusterstate.json
增加一台Solr,从leader出复制相同的数据,然后配置写到/clusterstate.json
查询
去中心的,leader和非leader一样功能,Replica接收搜索请求时,从Zookeeper中获取该Replica对应的Collection及所有的Shard和Replica
将请求发送到该Collection下对应的Shard,然后负载均衡到对应的Replica
SolrCloud也有其他功能,比如Optimization,就是一个运行在leader机器的进程,复杂压缩索引和归并Segment;近实时搜索等。总体看SolrCloud解决了Solr Replication遇到的一些问题,比Sphinx更好用,更自动化。
一号店
很多大一点的厂商如果不自研搜索引擎的话,并没有使用SolrCloud,而多基于Solr/Lucence。比如一号店的分布式搜索解决方案,如下所示:
http://www.infoq.com/cn/articles/yhd-11-11-distributed-search-engine-architecture
Broker就相当于Proxy,扮演了路由功能,很多厂商做的与一号店有相似之处。因为没有leader选举,所以索引的更新就由其他模块来做了。
ElasticSearch的倒排索引也是基于Lucence实现的。功能强大,不仅提供了实时搜索功能,还有分析功能,DB-Engines上面的搜索引擎排名,目前已经超越Solr排名第一位了。因为太强大了,功能也特别多,我研究还不够深,简单说下吧。
es会将集群名字相同的机器归为一个集群(业务),所以先说下启动过程。
当ElasticSearch的节点启动后,它会利用多播(multicast)(或者单播,如果用户更改了配置)寻找集群中的其它节点,并与之建立连接。
与SolrCloud相似,也是去中心化的,但是没有使用Zookeeper,而是自己实现了分布式锁,选主的流程叫做 ZenDiscovery(详情见第三个参考链接):
选举完leader后,主节点leader会去读取集群状态信息;因为主节点会监控其他节点,当其他节点出现故障时,会进行恢复工作。在这个阶段,主节点会去检查哪些分片可用,决定哪些分片作为主分片。
分片
es在创建索引时,自己设置好分片个数,默认5个,整个过程类似于分裂的概念,如下图所示:
至于读写、写操作等于SolrCloud相似,等我细研究后后续再说吧,也可以说下实时索引怎么做的,细节很多,下次再说吧。至于文中为什么不说Lucence,因为Lucence其实就是个index lib,只是解决倒排、正排索引怎么存放的,并不是一个完整的搜索引擎解决方案。而ES为什么能脱颖而出的主要原因是配套设施完善,工具,Web UI都是非常赞的,对于很多开源产品,它能后来居上的主要原因就是它真实的能解决用户遇到的问题或者比其他产品更好用。搜索引擎发展这么多年了,架构这块能做的基本上大家都差不太多,最后能脱颖而出的肯定是第三方工具做的更完善,更好用的了。
PS:
至于阿里搜索怎么做的,可以参考下这个文档,包括了阿里搜索里用到的很多基础模块了:
https://share.weiyun.com/f66e79d9f6897d0aac683361531cf00d
参考链接:
http://blog.haohtml.com/archives/13724
http://www.voidcn.com/blog/u011026968/article/p-4922079.html
http://jolestar.com/elasticsearch-architecture/
GCC(GNU Compiler Collection,GNU编译器套装),是一套由 GNU 开发的编程语言编译器。它是一套以 GPL 及 LGPL 许可证所发行的自由软件,也是 GNU计划 的关键部分,亦是自由的 类Unix 及苹果计算机 Mac OS X 操作系统的标准编译器。GCC(特别是其中的C语言编译器)也常被认为是跨平台编译器的事实标准。在ubuntu等操作系统中编译安装大多数开源项目几乎都会用到gcc。
|
|
简单的编译: gcc hello.c -o hello
gcc 编译器就会为我们生成一个 hello 的可执行文件,执行./hello 就可以看到程序的输出结果了。命令行中 gcc 表示我们是用 gcc 来编译我们的源程序,-o 选项表示我们要求编译器 给我们输出的可执行文件名为 hello 而 hello.c 是我们的源程序文件。
实质上,上述编译过程是分为四个阶段进行的,即预处理(也称预编译,Preprocessing)、编译(Compilation)、汇编 (Assembly)和连接(Linking)。
gcc还有很多参数:
-o 要求输出的可执行文件名。
-c 要求编译器输出目标代码,而不必要输出可执行文件。
-g 要求编译器在编译的时候提供我们以后对程序进行调试的信息。
比如hello.c include了一些头,包含在hello1.h和hello2.h中,hello1.c和hello2.c也包含了这些头,需要编译多个文件
gcc -c hello.c
gcc -c hello1.c
gcc -c hello2.c
gcc hello.o hello1.o hello2.o -o hello
虽然这些命令可以使用shell脚本完成,但是大型的项目中会有很多源程序,不可能有一个改动就重新一个个编译。
因此就有了makefile,makefile带来的好处就是自动化编译,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
在 Makefile 中也#开始的行都是注释行.Makefile 中 最重要的是描述文件的依赖关系的说明.一般的格式是:
target: prerequisites
TAB command
第一行表示的是依赖关系,第二行是规则。
于是 hello的生成可以这么写
hello:hello.o hello1.o hello2.o
gcc -o hello hello.o hello1.o hello2.o
hello.o:hello.c hello1.h hello2.h
gcc -c hello.c
hello1.o:hello1.c hello1.h
gcc -c hello1.c
hello2.o:hello2.c hello2.h
gcc -c hello2.c
Makefile 有三个非常有用的变量.分别是$@,$^,$<代表的意义分别是: $@–目标文件,$^–所有的依赖文件,$<–第一个依赖文件。
所以上面的可以写成
hello:hello.o hello1.o hello2.o
gcc -o $@ $^
.c.o:
gcc -c $<
gdb工具,暂时没用到,先不看了
简单地了解下linux下C语言编程。有空得继续深入了
有些人也喜欢叫发号器,一般是作为服务运行,主要目的是就是为一个系统的数据对象分配一个唯一id。
一般来说,id生成器的要求主要是一下几点:
这里不讨论uuiduuid-vs-int-insert-performance
这种方式可以利用mysql的自增id。但是使用mysql有一些瓶颈:
flickr巧妙的利用replace实现分布式发号器
自增id使用big int 为了提高并发,使用多台mysql机器,每台机器设置不同的起始值和步长,sql使用replace唯一id的方式使得mysql每次增长一个步长(insert on duplacate key也可以,update不可以,是因为update的返回值是成功还是失败)
表结构
|
|
sql语句
|
|
设置步长和起始值(步长也可以直接在session中指定)
|
|
php 实现,java无法使用jpa,可以用jdbcTemplate实现
|
|
优缺点
优点是简单,实现快速。
缺点是mysql中需要设置起始值,需要多台mysql服务,可扩展性差。并发取决于mysql节点数量
架构图
腾讯的seqsvr 万亿级调用系统:微信序列号生成器架构设计及演变
简单来讲,seqsvr是一个巨大的long数组,每个元素代表一个用户,比如小明的uid是12,那么seq[12]就是他产生的guid,每个用户(元素)之间产生的uid互不影响,每个uid产生的guid严格递增(为了保证这一点,付出的代价很高,不能使用负载均衡,同一时间只有一个服务器产生该用户的guid),发号分为两层,下层是持久化层,主要存储当前uid产生的最大的guid,上层为缓存层,缓存一个步长,用来发号。也做了很多优化,比如设置最大临时guid,相邻的uid共享该max_guid,这样在服务器重启时可以大大减少向存储层发起的请求,减少内存使用。容灾问题上做了很多处理,前面说到这种方式存在单点问题,seqsrv做了很多,分set做隔离,路由和配置服务做失败请求,仲裁服务做服务切换
主流的方式都是类似twitter的snowflake,一个long是64字节,可以根据自身业务特点选择秒级别还是毫秒级别,一般系统寿命在30年左右,所以毫秒级别需要41位表示时间,秒级别需要30位,剩下的需要几位去表示机器号,也可以预留2位表示版本,方便切换
|
|
snowflake
优点:可用性强
缺点:依赖zookeeper,要么人肉做配置,总之需要在服务中配置机器号识别。
Instagram
sharding-ids-at-instagram
|
|
也是类似snowflake,直接在postgrsql中实现的。简单看了一下,类似触发器,id自增换成函数生成
达达的也类似达达-高性能服务端优化之路
优: 开发成本低
劣: 基于postgreSQL的存储过程,较为偏门
基于redis的id生成器
基于redis的id生成器(http://blog.csdn.net/hengyunabc/article/details/44244951)
优点:利用redis lua脚本实现,少量机器即可满足需求
缺点: 需要配置redis起始值及步长
一些变种
Hibernate github
|
|
mongodb Mongdodb objectId 非long型
|
|
flickr Ticket Servers: Distributed Unique Primary Keys on the Cheap
日常应用产生的日志是需要做文件分割的,无论是按文件大小还是按时间
## 利用shell脚本
实现了每1000行来分割文件
split 参数:
-b : 后面可接欲分割成的档案大小,可加单位,例如 b, k, m 等;
-l : 以行数来进行分割;
-d : 默认是以字符为后缀的,使用-d 或者–numeric-suffixes 可以改变后缀
查看参数更多可以 man split
1.按每个文件1000行来分割除
split -l 1000 access.log split
会切割成splitaa splitab splitac…
2.按照每个文件100K来分割
split -l 1000 access.log split
会切割成httpaa,httpab,httpac ……..
3.流切割
对于流split一样可以做到
简单点举例
cat access.log | split -l 1000 split
这样有些守护进程就可以这样写
php xxx.php | split -l 10000 xxx.log.
文章转自数据结构之并查集;
并查集(Disjoint set或者Union-find set)是一种树型的数据结构,常用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。
并查集是一种非常简单的数据结构,它主要涉及两个基本操作,分别为:
合并两个不相交集合
判断两个元素是否属于同一个集合
1.合并两个不相交集合(Union(x,y))
合并操作很简单:先设置一个数组Father[x],表示x的“父亲”的编号。那么,合并两个不相交集合的方法就是,找到其中一个集合最父亲的父亲(也就是最久远的祖先),将另外一个集合的最久远的祖先的父亲指向它。
上图为两个不相交集合,b图为合并后Father(b):=Father(g)
2.判断两个元素是否属于同一集合(Find_Set(x))
本操作可转换为寻找两个元素的最久远祖先是否相同。可以采用递归实现。
1.Find_Set(x)时,路径压缩
寻找祖先时,我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度。为了避免这种情况,我们需对路径进行压缩,即当我们经过”递推”找到祖先节点后,”回溯”的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了,如下图所示。可见,路径压缩方便了以后的查找。
2.Union(x,y)时,按秩合并
即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。
|
|
空间复杂度为O(N),建立一个集合的时间复杂度为O(1),N次合并M查找的时间复杂度为O(M Alpha(N)),这里Alpha是Ackerman函数的某个反函数,在很大的范围内(人类目前观测到的宇宙范围估算有10的80次方个原子,这小于前面所说的范围)这个函数的值可以看成是不大于4的,所以并查集的操作可以看作是线性的。具体复杂度分析过程见参考资料(3)。
并查集常作为另一种复杂的数据结构或者算法的存储结构。常见的应用有:求无向图的连通分量个数,最近公共祖先(LCA),带限制的作业排序,实现Kruskar算法求最小生成树等。
并查集:http://www.nocow.cn/index.php/%E5%B9%B6%E6%9F%A5%E9%9B%86
博文《并查集详解》:http://www.cnblogs.com/cherish_yimi/
Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and Clifford Stein. Introduction to Algorithms, Second Edition. MIT Press and McGraw-Hill, 2001. ISBN 0-262-03293-7. Chapter 21: Data structures for Disjoint Sets, pp. 498–524.
文章转自SHELL编程之内建命令
微博ID:orroz
微信公众号:Linux系统技术
本文是shell编程系列的第五篇,集中介绍了bash相关内建命令的使用。通过学习本文内容,可以帮你解决以下问题:
内建命令是指bash内部实现的命令。bash在执行这些命令的时候不同于一般外部命令的fork、exec、wait的处理过程,这内建功能本身不需要打开一个子进程执行,而是bash本身就可以进行处理。分析外部命令的执行过程我们可以理解内建命令的重要性,外建命令都会打开一个子进程执行,所以有些功能没办法通过外建命令实现。比如当我们想改变当前bash进程的某些环境的时候,如:切换当前进程工作目录,如果打开一个子进程,切换之后将会改变子进程的工作目录,与当前bash没关系。所以内建命令基本都是从必须放在bash内部实现的命令。bash所有的内建命令只有50多个,绝大多数的命令我们在之前的介绍中都已经使用过了。下面我们就把它们按照使用的场景分类之后,分别介绍一下在bash编程中可能会经常用到的内建命令。
对于任何编程语言来说,程序跟文件的输入输出都是非常重要的内容,bash编程当然也不例外。所有的shell编程与其他语言在IO处理这一块的最大区别就是,shell可以直接使用命令进行处理,而其他语言基本上都要依赖IO处理的库和函数进行处理。所以对于shell编程来说,IO处理的相关代码写起来要简单的多。本节我们只讨论bash内建的IO处理命令,而外建的诸如grep、sed、awk这样的高级处理命令不在本文的讨论范围内。
source:
.:
以上两个命令:source和.实际上是同一个内建命令,它们的功能完全一样,只是两种不同写法。我们都应该见过这样一种写法,如:
|
|
这里的”. $i”实际上就是source $i。这个命令的含义是:读取文件的内容,并在当前bash环境下将其内容当命令执行。注意,这与输入一个可执行脚本的路径的执行方式是不同的。路径执行的方式会打开一个子进程的bash环境去执行脚本中的内容,而source方式将会直接在当前bash环境中执行其内容。所以这种方式主要用于想引用一个脚本中的内容用来改变当前bash环境。如:加载环境变量配置脚本或从另一个脚本中引用其定义的函数时。我们可以通过如下例子来理解一下这个内建命令的作用:
|
|
我们可以通过以上例子中的$aaa变量看到当前bash环境的变化,可以通过$$变量,看到不同执行过程的进程环境变化。
read:
这个命令可以让bash从标准输入读取输字符串到一个变量中。用法如下:
|
|
程序执行结果:
|
|
我们可以利用read命令实现一些简单的交互程序。read自带提示输出功能,-p参数可以让read在读取输入之前先打印一个字符串。read命令除了可以读取输入并赋值一个变量以外,还可以赋值一个数组,比如我们想把一个命令的输出读到一个数组中,使用方法是:
|
|
执行结果:
|
|
输入为:111 222 333 444 555,就会打印出整个数组列表。
mapfile:
readarray:
这两个命令又是同一个命令的两种写法。它们的功能是,将一个文本文件直接变成一个数组,每行作为数组的一个元素。这对某些程序的处理是很方便的。尤其是当你要对某些文件进行全文的分析或者处理的时候,比一行一行读进来处理方便的多。用法:
|
|
程序输出:
|
|
本例子中使用了-u参数,表示让mapfile或readarray命令从一个文件描述符读取,如果不指定文件描述符,命令将默认从标准输入读取。所以很多人可能习惯用管道的方式读取,如:
|
|
但是最后却发现passwd变量根本不存在。这个原因是:如果内建命令放到管道环境中执行,那么bash会给它创建一个subshell进行处理。于是创建的数组实际上与父进程没有关系。这点是使用内建命令需要注意的一点。同样,read命令也可能会出现类似的使用错误。如:
|
|
执行完之后,我们在bash脚本环境中仍然无法读取到test变量的值,也是同样的原因。
mapfile的其他参数,大家可以自行参考help mapfile或help readarray取得帮助。
echo:
printf:
这两个都是用来做输出的命令,其中echo是我们经常使用的,就不啰嗦了,具体参数可以help echo。printf命令是一个用来进行格式化输出的命令,跟C语言或者其他语言的printf格式化输出的方法都类似,比如:
|
|
使用很简单,具体也请参见:help printf。
作业控制指的是jobs功能。一般情况下bash执行命令的方式是打开一个子进程并wait等待其退出,所以bash在等待一个命令执行的过程中不能处理其他命令。而jobs功能给我们提供了一种办法,可以让bash不用显示的等待子进程执行完毕后再处理别的命令,在命令行中使用这个功能的方法是在命令后面加&符号,表明进程放进作业控制中处理,如:
|
|
我们放了5个sleep进程进入jobs作业控制。大家可以当作这是bash提供给我们的一种“并发处理”方式。此时我们可以使用jobs命令查看作业系统中有哪些进程在执行:
|
|
除了数字外,这里还有+和-号标示。+标示当前作业任务,-表示备用的当前作业任务。所谓的当前作业,就是最后一个被放到作业控制中的进程,而备用的则是当前进程如果退出,那么备用的就会变成当前的。这些jobs进程可以使用编号和PID的方式控制,如:
|
|
表示杀掉1号作业任务,还可以使用kill %+或者kill %-以及kill %%(等同于%+)。除了可以kill这些进程以外,bash还提供了其他控制命令:
fg:
bg:
将指定的作业进程回到前台让当前bash去wait。如:
|
|
于是当前bash又去“wait”5号作业任务了。当然fg后面也可以使用%%、%+、%-等符号,如果fg不加参数效果跟fg %+也是一样的。让一个当前bash正在wait的进程回到作业控制,可以使用ctrl+z快捷键,这样会让这个进程处于stop状态:
[zorro@zorrozou-pc0 bash]$ fg %5
sleep 3000
^Z
[5]+ Stopped sleep 3000
[zorro@zorrozou-pc0 bash]$ jobs
[2] Running sleep 3000 &
[3] Running sleep 3000 &
[4]- Running sleep 3000 &
[5]+ Stopped sleep 3000
这个进程目前是stopped的,想让它再运行起来可以使用bg命令:
|
|
disown:
disown命令可以让一个jobs作业控制进程脱离作业控制,变成一个“野”进程:
|
|
直接回车的效果跟diswon %+是一样的,也是处理当前作业进程。这里要注意的是,disown之后的进程仍然是还在运行的,只是bash不会wait它,jobs中也不在了。
进程在系统中免不了要处理信号,即使是bash。我们至少需要使用命令给别进程发送信号,于是就有了kill命令。kill这个命令应该不用多说了,但是需要大家更多理解的是信号的概念。大家可以使用kill -l命令查看信号列表:
|
|
每个信号的意思以及进程接收到相关信号的默认行为了这个内容大家可以参见《UNIX环境高级编程》。我们在此先只需知道,常用的信号有2号(crtl c就是发送2号信号),15号(kill默认发送),9号(著名的kill -9)这几个就可以了。其他我们还需要知道,这些信号绝大多数是可以被进程设置其相应行为的,除了9号和19号信号。这也是为什么我们一般用kill直接无法杀掉的进程都会再用kill -9试试的原因。
那么既然进程可以设置信号的行为,bash中如何处理呢?使用trap命令。方法如下:
|
|
trap命令的格式如下:
|
|
在我们的例子中,第一个trap命令的意思是,定义针对2号和15号信号的行为,当进程接收到这两个信号的时候,将执行echo hello。第二个trap的意思是,如果进程接收到3号信号将执行exit 17,以17为返回值退出进程。然后我们来看一下进程执行的效果:
|
|
此时按ctrl+c和kill这个bash进程都会让进程打印hello。3号信号可以用ctrl+\发送:
|
|
此时进程退出,返回值是17,而不是128+3=131。这就是trap命令的用法。
suspend:
bash还提供了一种让bash执行暂停并等待信号的功能,就是suspend命令。它等待的是18号SIGCONT信号,这个信号本身的含义就是让一个处在T(stop)状态的进程恢复运行。使用方法:
|
|
执行效果:
|
|
十秒之后:
|
|
以上是suspend在脚本中的使用方法。另外,suspend默认不能在非loginshell中使用,如果使用,需要加-f参数。
bash中也实现了基本的进程控制方法。主要的命令有exit,exec,logout,wait。其中exit我们已经了解了。logout的功能跟exit实际上差不多,区别只是logout是专门用来退出login方式的bash的。如果bash不是login方式执行的,logout会报错:
|
|
wait命令的功能是用来等待jobs作业控制进程退出的。因为一般进程默认行为就是要等待其退出之后才能继续执行。wait可以等待指定的某个jobs进程,也可以等待所有jobs进程都退出之后再返回,实际上wait命令在bash脚本中是可以作为类似“屏障”这样的功能使用的。考虑这样一个场景,我们程序在运行到某一个阶段之后,需要并发的执行几个jobs,并且一定要等到这些jobs都完成工作才能继续执行,但是每个jobs的运行时间又不一定多久,此时,我们就可以用这样一个办法:
|
|
通过这个例子可以看到wait的行为:在不加任何参数的情况下,wait会等到所有作业控制进程都退出之后再回返回,否则就会一直等待。当然,wait也可以指定只等待其中一个进程,可以指定pid和jobs方式的作业进程编号,如%3,就变成了:
|
|
我们已经在重定向那一部分讲过exec处理bash程序的文件描述符的使用方法了,在此补充一下它是如何执行命令的。这个命令的执行过程跟exec族的函数功能是一样的:将当前进程的执行镜像替换成指定进程的执行镜像。还是举例来看:
|
|
实际上这个脚本在执行到exec ls /etc/passwd之后,bash进程就已经替换为ls进程了,所以后续的echo命令都不会执行,ls执行完,这个进程就完全退出了。
我们已经学习过使用shift方式处理命令行参数了,但是这个功能还是比较简单,它每次执行就仅仅是将参数左移一位而已,将本次的$2变成下次的$1。bash也给我们提供了一个更为专业的命令行参数处理方法,这个命令是getopts。
我们都知道一般的命令参数都是通过-a、-b、-c这样的参数来指定各种功能的,如果我们想要实现这样的功能,只单纯使用shift这样的方式手工处理将会非常麻烦,而且还不能支持让-a -b写成-ab这样的方式。bash跟其他语言一样,提供了getopts这样的方法来帮助我们处理类似的问题,如:
|
|
以下为程序输出:
|
|
getopts只能处理段格式参数,如:-a这样的。不能支持的是如–login这种长格式参数。实际上我们的系统中还给了一个getopt命令,可以处理长格式参数。这个命令不是内建命令,使用方法跟getopts类似,大家可以自己man getopt近一步学习这个命令的使用,这里就不再赘述了。
内建命令中最多的就是关于进程环境的配置的相关命令,当然绝大多数我们之前已经会用了。它们包括:alias、unalias、cd、declare、typeset、dirs、enable、export、hash、history、popd、pushd、local、pwd、readonly、set、unset、shopt、ulimit、umask。
我们在这需要简单说明的命令有:
declare:
typeset:
这两个命令用来声明或显示进程的变量或函数相关信息和属性。如:
declare -a array:可以声明一个数组变量。
declare -A array:可以声明一个关联数组。
declare -f func:可以声明或查看一个函数。
其他常用参数可以help declare查看。
enable:
可以用来打开或者关闭某个内建命令的功能。
dirs:
popd:
pushd:
dirs、popd、pushd可以用来操作目录栈。目录栈是bash提供的一种纪录曾经去过的相关目录的缓存数据结构,可以方便的使操作者在多个深层次的目录中方便的跳转。使用演示:
显示当前目录栈:
|
|
只有一个当前工作目录。将aaa加入目录栈:
|
|
pushd除了将目录加入了目录栈外,还改变了当前工作目录。
|
|
将bbb目录加入目录栈:
|
|
加入ccc、ddd、eee目录:
|
|
将当前工作目录切换到目录栈中的第2个目录,即当前的ddd目录:
|
|
将当前工作目录切换到目录栈中的第5个目录,即当前的~/bash/dirstack目录:
|
|
+N表示当前目录栈从左往右数的第N个,第一个是左边的第一个目录,从0开始。
将当前工作目录切换到目录栈中的倒数第3个目录,即当前的ddd目录:
|
|
-N表示当亲啊目录栈从右往左数的第N个,第一个是右边的第一个目录,从0开始。
从目录栈中推出一个目录,默认推出当前所在的目录:
|
|
指定要推出的目录编号,数字含义跟pushd一样:
|
|
readonly:
声明一个只读变量。
local:
声明一个局部变量。bash的局部变量概念很简单,它只能在函数中使用,并且局部变量只有在函数中可见。
set:
shopt:
我们之前已经讲过这两个命令的使用。这里补充一下其他信息,请参见:http://www.cnblogs.com/ziyunfei/p/4913758.html
eval:
eval是一个可能会被经常用到的内建命令。它的作用其实很简单,就是将指定的命令解析两次。可以这样理解这个命令:
首先我们定义一个变量:
|
|
这个变量时pipe,值就是”|”这个字符。然后我们试图在后续命令中引入管道这个功能,但是管道符是从变量中引入的,如:
|
|
此时执行报错了,因为bash在解释这条命令的时候,并不会先将$pipe解析成”|”再做解释。这时候我们需要让bash先解析$pipe,然后得到”|”字符之后,再将cat /etc/passwd | wc -l当成一个要执行的命令传给bash解释执行。此时我们需要eval:
|
|
这就是eval的用法。再来理解一下,eval就是将所给的命令解析两遍。
最后
通过本文和之前的文章,我们几乎将所有的bash内建命令都覆盖到了。本文主要包括的知识点为:
文章转自SHELL编程之特殊符号
微博ID:orroz
微信公众号:Linux系统技术
本文是shell编程系列的第四篇,集中介绍了bash编程可能涉及到的特殊符号的使用。学会本文内容可以帮助你写出天书一样的bash脚本,并且顺便解决以下问题:
这里需要额外注意的是,相同的符号出现在不同的上下文中可能会有不同的含义。我们会在后续的讲解中突出它们的区别。
重定向也叫输入输出重定向。我们先通过基本的使用对这个概念有个感性认识。
大家应该都用过cat命令,可以输出一个文件的内容。如:cat /etc/passwd。如果不给cat任何参数,那么cat将从键盘(标准输入)读取用户的输入,直接将内容显示到屏幕上,就像这样:
|
|
可以通过输入重定向让cat命令从别的地方读取输入,显示到当前屏幕上。最简单的方式是输入重定向一个文件,不过这不够“神奇”,我们让cat从别的终端读取输入试试。我当前使用桌面的终端terminal开了多个bash,使用ps命令可以看到这些终端所占用的输入文件是哪个:
|
|
通过第二列可以看到,不同的bash所在的终端文件是哪个,这里的pts/3就意味着这个文件放在/dev/pts/3。我们来试一下,在pts/2对应的bash中输入:
|
|
然后切换到pts/3所在的bash上敲入字符串,在pts/2的bash中能看见相关字符:
|
|
这只是个输入重定向的例子,一般我们也可以直接cat < /etc/passwd,表示让cat命令不是从默认输入读取,而是从/etc/passwd读取,这就是输入重定向,使用”<“。
绝大多数命令都有输出,用来显示给人看,所以输出基本都显示在屏幕(终端)上。有时候我们不想看到,就可以把输出重定向到别的地方:
[zorro@zorrozou-pc0 bash]$ ls /
bin boot cgroup data dev etc home lib lib64 lost+found mnt opt proc root run sbin srv sys tmp usr var
[zorro@zorrozou-pc0 bash]$ ls / > /tmp/out
[zorro@zorrozou-pc0 bash]$ cat /tmp/out
bin
boot
cgroup
data
dev
……
使用一个”>”,将原本显示在屏幕上的内容给输出到了/tmp/out文件中。这个功能就是输出重定向。
命令执行都会遇到错误,一般也都是给人看的,所以默认还是显示在屏幕上。这些输出使用”>”是不能进行重定向的:
[zorro@zorrozou-pc0 bash]$ ls /1234 > /tmp/err
ls: cannot access ‘/1234’: No such file or directory
可以看到,报错还是显示在了屏幕上。如果想要重定向这样的内容,可以使用”2>”:
[zorro@zorrozou-pc0 bash]$ ls /1234 2> /tmp/err
[zorro@zorrozou-pc0 bash]$ cat /tmp/err
ls: cannot access ‘/1234’: No such file or directory
以上就是常见的输入输出重定向。在进行其它技巧讲解之前,我们有必要理解一下重定向的本质,所以要先从文件描述符说起。
文件描述符简称fd,它是一个抽象概念,在很多其它体系下,它可能有其它名字,比如在C库编程中可以叫做文件流或文件流指针,在其它语言中也可以叫做文件句柄(handler),而且这些不同名词的隐含意义可能是不完全相同的。不过在系统层,还是应该使用系统调用中规定的名词,我们统一把它叫做文件描述符。
文件描述符本质上是一个数组下标(C语言数组)。在内核中,这个数组是用来管理一个进程打开的文件的对应关系的数组。就是说,对于任何一个进程来说,都有这样一个数组来管理它打开的文件,数组中的每一个元素和文件是映射关系,即:一个数组元素只能映射一个文件,而一个文件可以被多个数组元素所映射。
|
|
shell在产生一个新进程后,新进程的前三个文件描述符都默认指向三个相关文件。这三个文件描述符对应的数组下标分别为0,1,2。0对应的文件叫做标准输入(stdin),1对应的文件叫做标准输出(stdout),2对应的文件叫做标准报错(stderr)。但是实际上,默认跟人交互的输入是键盘、鼠标,输出是显示器屏幕,这些硬件设备对于程序来说都是不认识的,所以操作系统借用了原来“终端”的概念,将键盘鼠标显示器都表现成一个终端文件。于是stdin、stdout和stderr就最重都指向了这所谓的终端文件上。于是,从键盘输入的内容,进程可以从标准输入的0号文件描述符读取,正常的输出内容从1号描述符写出,报错信息被定义为从2号描述符写出。这就是标准输入、标准输出和标准报错对应的描述符编号是0、1、2的原因。这也是为什么对报错进行重定向要使用2>的原因(其实1>也是可以用的)。
明白了以上内容之后,很多重定向的数字魔法就好理解了,比如:
|
|
这相当于只看报错信息。
|
|
这相当于只看正确输出信息。
|
|
将标准报错输出的,重定向到标准输出再输出。
|
|
“>>”表示追加重定向。
相信大家对&>>、1>&2、?2>&3、6>&8、>>file 2>&1这样的写法应该也都能理解了。进程可以打开多个文件,多个描述符之间都可以进行重定向。当然,输入也可以,比如:3<表示从描述符3读取。下面我们罗列一下其他重定向符号和用法:
Here Document:
语法:
|
|
这是一种特殊的输入重定向,重定向的内容并不是来自于某个文件,而是从当前输入读取,直到输入中写入了delimiter字符标记结束。用法:
|
|
这个例子可以看到,最后cat输出的内容都是在上面写入的内容,而且内容中不包括EOF,因为EOF是标记输入结束的字符串。这个功能在脚本中通常可以用于需要交互式处理的某些命令的输入和文件编辑,比如想在脚本中使用fdisk命令新建一个分区:
|
|
当然这个脚本大家千万不要乱执行,可能会修改你的分区表。其中要输入的内容,相信熟悉fdisk命令的人应该都能明白,我就不多解释了。
Here strings:
语法:
|
|
使用方式:
|
|
其实就是将<<<符号后面的字符串当成要输入的内容给cat,而不是定向一个文件描述符。这样是不是就相当于把cat当echo用了?
文件描述符的复制:
复制输入文件描述符:[n]<&word
如果n没有指定数字,则默认复制0号文件描述符。word一般写一个已经打开的并且用来作为输入的描述符数字,表示将制订的n号描述符在制定的描述符上复制一个。如果word写的是“-”符号,则表示关闭这个文件描述符。如果word指定的不是一个用来输入的文件描述符,则会报错。
复制输出文件描述符:[n]>&word
复制一个输出的描述符,字段描述参考上面的输入复制,例子上面已经讲过了。这里还需要知道的就是1>&-表示关闭1号描述符。
文件描述符的移动:
移动输入描述符:[n]<&digit-
移动输出描述符:[n]>&digit-
这两个符号的意思都是将原有描述符在新的描述符编号上打开,并且关闭原有描述符。
描述符新建:
新建一个用来输入的描述符:[n]<word
新建一个用来输出的描述符:[n]>word
新建一个用来输入和输出的描述符:[n]<>word
word都应该写一个文件路径,用来表示这个文件描述符的关联文件是谁。
下面我们来看相关的编程例子:
|
|
以上脚本要注意的地方是,一般输入输出重定向都是放到命令后面作为后缀使用,所以如果单纯改变脚本的描述符,需要在前面加exec命令。这种用法也叫做描述符魔术。某些特殊符号还有一些特殊用法,比如:
|
|
表示清空文件,当然也可以写成
|
|
因为”:”是一个内建命令,跟true是同样的功能,所以没有任何输出,所以这个命令清空文件的作用。
我们在之前的例子中已经简单看过相关参数处理的特殊符号了,再来看一下:
|
|
执行结果:
|
|
可以罗列一下:
$0:命令名。
$n:n是一个数字,表示第n个参数。
$#:参数个数。
$*:所有参数列表。
$@:同上。
实际上大家可以认为上面的0,1,2,3,#,*,@,?都是一堆变量名。跟aaa=1000定义的变量没什么区别,只是他们有特殊含义。所以$@实际上就是对@变量取值,跟$aaa概念一样。所以上述所有取值都可以写成${}的方式,因为bash中对变量取值有两种写法,另外一种是${aaa}。这种写法的好处是对变量名字可以有更明确的界定,比如:
|
|
内建命令shift可以用来对参数进行位置处理,它会将所有参数都左移一个位置,可以用来进行参数处理。使用例子如下:
|
|
执行效果:
|
|
其他的特殊变量还有:
$?:上一个命令的返回值。
$$:当前shell的PID。
$!:最近一个被放到后台任务管理的进程PID。如:
|
|
$-:列出当前bash的运行参数,比如set -v或者-i这样的参数。
$:”“算是所有特殊变量中最诡异的一个了,在bash脚本刚开始的时候,它可以取到脚本的完整文件名。当执行完某个命令之后,它可以取到,这个命令的最后一个参数。当在检查邮件的时候,这个变量帮你保存当前正在查看的邮件名。
bash中可以定义数组,使用方法如下:
|
|
用类似枚举的方式创建一些目录:
|
|
可能还有这样用的:
|
|
这个命令的意思是:mv test/a.conf test/c.conf
~:在bash中一般表示用户的主目录。cd ~表示回到主目录。cd ~zorro表示回到zorro用户的主目录。
变量扩展
我们都知道取一个变量值可以用$或者${}。在使用${}的时候可以添加很多对变量进行扩展操作的功能,下面我们就分别来看看。
${aaa:-1000}
这个表示如果变量aaa是空值或者没有赋值,则此表达式取值为1000,aaa变量不被更改,以后还是空。如果aaa已经被赋值,则原值不变:
|
|
${aaa:=1000}
跟上面的表达式的区别是,如果aaa未被赋值,则赋值成=后面的值,其他行为不变:
|
|
${aaa:?unset}
判断变量是否未定义或为空,如果符合条件,就提示?后面的字符串。
|
|
${aaa:+unset}
如果aaa为空或者未设置,则什么也不做。如果已被设置,则取+后面的值。并不改变原aaa值:
|
|
${aaa:10}
取字符串偏移量,表示取出aaa变量对应字符串的第10个字符之后的字符串,变量原值不变。
|
|
${aaa:10:15}
第二个数字表示取多长:
|
|
${!B*}
${!B@}
取出所有以B开头的变量名(请注意他们跟数组中相关符号的差别):
|
|
${ #aaa }
取变量长度:
|
|
${ parameter#word}
变量paramenter看做字符串从左往右找到第一个word,取其后面的字串:
|
|
这里需要注意的是,word必须是一个路径匹配的字符串,比如:
|
|
这个表示删除路径中匹配到的第一个zorro左边的所有字符,而这样是无效的:
|
|
因为此时zorro不是一个路径匹配。另外,这个表达式只能删除匹配到的左边的字符串,保留右边的。
${ parameter##word}
这个表达式与上一个的区别是,匹配的不是第一个符合条件的word,而是最后一个:
|
|
${ parameter%word}
${ parameter%%word}
这两个符号相对于上面两个相当于#号换成了%号,操作区别也从匹配删除左边的字符变成了匹配删除右边的字符,如:
|
|
以上#号和%号分别是匹配删除哪边的,容易记不住。不过有个窍门是,可以看看他们分别在键盘上的$的哪边?在左边的就是匹配删除左边的,在右边就是匹配删除右边的。
${ parameter/pattern/string}
字符串替换,将pattern匹配到的第一个字符串替换成string,pattern可以使用通配符,如:
|
|
${ parameter//pattern/string}
意义同上,不过变成了全局替换:
|
|
${parameter^pattern}
${parameter^^pattern}
${parameter,pattern}
${parameter,,pattern}
[zorro@zorrozou-pc0 bash]$ echo $aaa
abcdefg
[zorro@zorrozou-pc0 bash]$ echo ${aaa^}
Abcdefg
[zorro@zorrozou-pc0 bash]$ echo ${aaa^^}
ABCDEFG
[zorro@zorrozou-pc0 bash]$ aaa=ABCDEFG
[zorro@zorrozou-pc0 bash]$ echo ${aaa,}
aBCDEFG
[zorro@zorrozou-pc0 bash]$ echo ${aaa,,}
abcdefg
[zorro@zorrozou-pc0 bash]$ echo ls
ls
[zorro@zorrozou-pc0 bash]$ echo ls
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh redirect.sh shift.sh until.sh
alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime select.sh test while.sh
[zorro@zorrozou-pc0 bash]$ echo $((123+345))
468
[zorro@zorrozou-pc0 bash]$
[zorro@zorrozou-pc0 bash]$
[zorro@zorrozou-pc0 bash]$ echo $((345-123))
222
[zorro@zorrozou-pc0 bash]$ echo $((345*123))
42435
[zorro@zorrozou-pc0 bash]$ echo $((345/123))
2
[zorro@zorrozou-pc0 bash]$ echo $((345%123))
99
[zorro@zorrozou-pc0 bash]$ i=1
[zorro@zorrozou-pc0 bash]$ echo $((i++))
1
[zorro@zorrozou-pc0 bash]$ echo $((i++))
2
[zorro@zorrozou-pc0 bash]$ echo $i
3
[zorro@zorrozou-pc0 bash]$ i=1
[zorro@zorrozou-pc0 bash]$ echo $((++i))
2
[zorro@zorrozou-pc0 bash]$ echo $((++i))
3
[zorro@zorrozou-pc0 bash]$ echo $i
3
id++ id–
++id –id
== !=
&
^
|
&&
||
expr?expr:expr
= *= /= %= += -= <<= >>= &= ^= |=
|
|
[zorro@zorrozou-pc0 bash]$ i=0
[zorro@zorrozou-pc0 bash]$ let ++i
[zorro@zorrozou-pc0 bash]$ echo $i
1
[zorro@zorrozou-pc0 bash]$ i=2
[zorro@zorrozou-pc0 bash]$ let i=i**2
[zorro@zorrozou-pc0 bash]$ echo $i
4
[zorro@zorrozou-pc0 bash]$ i=0
[zorro@zorrozou-pc0 bash]$ ((i++))
[zorro@zorrozou-pc0 bash]$ echo $i
1
[zorro@zorrozou-pc0 bash]$ ((i+=4))
[zorro@zorrozou-pc0 bash]$ echo $i
5
[zorro@zorrozou-pc0 bash]$ ((i=i**7))
[zorro@zorrozou-pc0 bash]$ echo $i
78125
[zorro@zorrozou-pc0 bash]$ diff <(df -h) <(df)
1,10c1,10
< Filesystem Size Used Avail Use% Mounted on
< dev 7.8G 0 7.8G 0% /dev
< run 7.9G 1.1M 7.8G 1% /run
< /dev/sda3 27G 13G 13G 50% /
< tmpfs 7.9G 500K 7.8G 1% /dev/shm
< tmpfs 7.9G 0 7.9G 0% /sys/fs/cgroup
< tmpfs 7.9G 112K 7.8G 1% /tmp
< /dev/mapper/fedora-home 99G 76G 18G 82% /home
< tmpfs 1.6G 16K 1.6G 1% /run/user/120
Filesystem 1K-blocks Used Available Use% Mounted on
dev 8176372 0 8176372 0% /dev
run 8178968 1052 8177916 1% /run
/dev/sda3 28071076 13202040 13420028 50% /
tmpfs 8178968 500 8178468 1% /dev/shm
tmpfs 8178968 0 8178968 0% /sys/fs/cgroup
tmpfs 8178968 112 8178856 1% /tmp
/dev/mapper/fedora-home 103081248 79381728 18440256 82% /home
tmpfs 1635796 16 1635780 1% /run/user/120
tmpfs 1635796 16 1635780 1% /run/user/1000
|
|
?(pattern-list):匹配所给pattern的0次或1次;
*(pattern-list):匹配所给pattern的0次以上包括0次;
+(pattern-list):匹配所给pattern的1次以上包括1次;
@(pattern-list):匹配所给pattern的1次;
!(pattern-list):匹配非括号内的所给pattern。
|
|
[zorro@zorrozou-pc0 bash]$ shopt -u extglob
[zorro@zorrozou-pc0 bash]$ ls /etc/(a)
/etc/netdata:
apps_groups.conf charts.d.conf netdata.conf
/etc/pcmcia:
config.opts
[zorro@zorrozou-pc0 bash]$ shopt -u extglob
[zorro@zorrozou-pc0 bash]$ ls /etc/(a)
-bash: syntax error near unexpected token `(‘
[zorro@zorrozou-pc0 bash]$ type !
! is a shell keyword
[zorro@zorrozou-pc0 bash]$ echo hello
hello
[zorro@zorrozou-pc0 bash]$ echo $?
0
[zorro@zorrozou-pc0 bash]$ ! echo hello
hello
[zorro@zorrozou-pc0 bash]$ echo $?
1
[root@zorrozou-pc0 zorro]# [[ -f /etc/passwd ]]
[root@zorrozou-pc0 zorro]# echo $?
0
[root@zorrozou-pc0 zorro]# [[ -f /etc/pass ]]
[root@zorrozou-pc0 zorro]# echo $?
1
[zorro@zorrozou-pc0 bash]$ ls
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh read.sh select.sh test while.sh
alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime redirect.sh shift.sh until.sh
[zorro@zorrozou-pc0 bash]$ touch *sh
[zorro@zorrozou-pc0 bash]$ touch *sh
[zorro@zorrozou-pc0 bash]$ ls
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh read.sh select.sh shift.sh until.sh
alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime redirect.sh ‘*sh’ test while.sh
[zorro@zorrozou-pc0 bash]$ touch \
[zorro@zorrozou-pc0 bash]$ ls
‘\’ alias.sh arg.sh auth_case.sh case.sh exit.sh for.sh if_1.sh na.sh prime redirect.sh ‘*sh’ test while.sh
3 arg1.sh array.sh auth_if.sh cat.sh for2.sh hash.sh name.sh ping.sh read.sh select.sh shift.sh until.sh
[zorro@zorrozou-pc0 bash]$ rm *sh
```
可以成功避免这种误操作的习惯是,不要用特殊字符作为文件名或者目录名,不要给自己犯错误的机会!
另外”也是非常重要的转义字符,\只能转义其后面的一个字符,而”可以转义其扩起来的所有字符。另外””也能起到一部分的转义作用,只是它的转义能力没有”强。”和
“”的区别是:”可以转义所有字符,而””不能对$字符、命令置换“和\转义字符进行转义。
先补充一个关于正则表达式的说明:
很多初学者容易将bash的特殊字符和正则表达式搞混,尤其是*、?、[]这些符号。实际上我们要明白,正则表达式跟bash的通配符和特殊符号没有任何关系。bash本身并不支持正则表达式。那些支持正在表达式的都是外部命令,比如grep、sed、awk这些高级文件处理命令。正则表达式是由这些命令自行处理的,而bash并不对正则表达式做任何解析和解释。
关于正则表达式的话题,我们就不在bash编程系列文章中讲解了,不过未来可能会在讲解sed、awk这样的高级文本处理命令中说明。
通过本文我们学习了bash的特殊符号相关内容,主要包括的知识点为: