Dunwu Blog

大道至简,知易行难

Java 容器之 List

ListCollection 的子接口,其中可以保存各个重复的内容。

List 简介

List 是一个接口,它继承于 Collection 的接口。它代表着有序的队列。

AbstractList 是一个抽象类,它继承于 AbstractCollectionAbstractList 实现了 List 接口中除 size()get(int location) 之外的函数。

AbstractSequentialList 是一个抽象类,它继承于 AbstractListAbstractSequentialList 实现了“链表中,根据 index 索引值操作链表的全部函数”。

ArrayList 和 LinkedList

ArrayListLinkedListList 最常用的实现。

  • ArrayList 基于动态数组实现,存在容量限制,当元素数超过最大容量时,会自动扩容;LinkedList 基于双向链表实现,不存在容量限制。
  • ArrayList 随机访问速度较快,随机插入、删除速度较慢;LinkedList 随机插入、删除速度较快,随机访问速度较慢。
  • ArrayListLinkedList 都不是线程安全的。

Vector 和 Stack

VectorStack 的设计目标是作为线程安全的 List 实现,替代 ArrayList

  • Vector - VectorArrayList 类似,也实现了 List 接口。但是, Vector 中的主要方法都是 synchronized 方法,即通过互斥同步方式保证操作的线程安全。
  • Stack - Stack 也是一个同步容器,它的方法也用 synchronized 进行了同步,它实际上是继承于 Vector 类。

ArrayList

ArrayList 是一个数组队列,相当于动态数组。**ArrayList 默认初始容量大小为 10 ,添加元素时,如果发现容量已满,会自动扩容为原始大小的 1.5 倍**。因此,应该尽量在初始化 ArrayList 时,为其指定合适的初始化容量大小,减少扩容操作产生的性能开销。

ArrayList 定义:

1
2
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

从 ArrayList 的定义,不难看出 ArrayList 的一些基本特性:

  • ArrayList 实现了 List 接口,并继承了 AbstractList,它支持所有 List 的操作。
  • ArrayList 实现了 RandomAccess 接口,支持随机访问RandomAccess 是一个标志接口,它意味着“只要实现该接口的 List 类,都支持快速随机访问”。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。
  • ArrayList 实现了 Cloneable 接口,默认为浅拷贝
  • ArrayList 实现了 Serializable 接口,支持序列化,能通过序列化方式传输。
  • ArrayList非线程安全的。

ArrayList 的数据结构

ArrayList 包含了两个重要的元素:elementDatasize

1
2
3
4
5
6
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 对象数组
transient Object[] elementData;
// 数组长度
private int size;
  • size - 是动态数组的实际大小,默认初始容量大小为 10。
  • elementData - 是一个 Object 数组,用于保存添加到 ArrayList 中的元素。正是由于实际存储元素的是 Object 数组,所以其天然支持随机访问。

ArrayList 构造方法

ArrayList 类实现了三个构造函数:

  • 第一个是默认构造方法,ArrayList 会创建一个空数组;
  • 第二个是创建 ArrayList 对象时,传入一个初始化值;
  • 第三个是传入一个集合类型进行初始化。

当 ArrayList 新增元素时,如果所存储的元素已经超过其当前容量,它会计算容量后再进行动态扩容。数组的动态扩容会导致整个数组进行一次内存复制。因此,初始化 ArrayList 时,指定数组初始大小,有助于减少数组的扩容次数,从而提高系统性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public ArrayList() {
// 创建一个空数组
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 根据初始化值创建数组大小
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 初始化值为 0 时,创建一个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

ArrayList 定制序列化

ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。为此,ArrayList 定制了其序列化方式。具体做法是:

  • 存储元素的 Object 数组(即 elementData)使用 transient 修饰,使得它可以被 Java 序列化所忽略。
  • ArrayList 重写了 writeObject()readObject() 来控制序列化数组中有元素填充那部分内容。

:bulb: 不了解 Java 序列化方式,可以参考:Java 序列化

ArrayList 访问元素

ArrayList 访问元素的实现主要基于以下关键性源码:

1
2
3
4
5
6
7
8
9
// 获取第 index 个元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}

E elementData(int index) {
return (E) elementData[index];
}

实现非常简单,其实就是**通过数组下标访问数组元素,其时间复杂度为 O(1)**,所以很快。

ArrayList 添加元素

ArrayList 添加元素有两种方法:一种是添加元素到数组末尾,另外一种是添加元素到任意位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 添加元素到数组末尾
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

// 添加元素到任意位置
public void add(int index, E element) {
rangeCheckForAdd(index);

ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}

两种添加元素方法的不同点是:

  • 添加元素到任意位置,会导致在该位置后的所有元素都需要重新排列
  • 而添加元素到数组末尾,在没有发生扩容的前提下,是不会有元素复制排序过程的。

两种添加元素方法的共同点是:添加元素时,会先检查容量大小,如果发现容量不足,会自动扩容为原始大小的 1.5 倍

ArrayList 添加元素的实现主要基于以下关键性源码:

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
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList 执行添加元素动作(add 方法)时,调用 ensureCapacityInternal 方法来保证容量足够。

  • 如果容量足够时,将数据作为数组中 size+1 位置上的元素写入,并将 size 自增 1。
  • 如果容量不够时,需要使用 grow 方法进行扩容数组,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。扩容操作实际上是调用 Arrays.copyOf() 把原数组拷贝为一个新数组,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

ArrayList 删除元素

ArrayList 的删除方法和添加元素到任意位置方法有些相似。

ArrayList 在每一次有效的删除操作后,都要进行数组的重组,并且删除的元素位置越靠前,数组重组的开销就越大。具体来说,ArrayList 会**调用 System.arraycopy()index+1 后面的元素都复制到 index 位置上。

1
2
3
4
5
6
7
8
9
10
11
12
13
public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

ArrayList 的 fail-fast

ArrayList 使用 modCount 来记录结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。

在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果发生改变,ArrayList 会抛出 ConcurrentModificationException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();

// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);

// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}

if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

LinkedList

LinkedList 基于双链表结构实现。由于是双链表,所以顺序访问会非常高效,而随机访问效率比较低。

LinkedList 定义:

1
2
3
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList 的定义,可以得出 LinkedList 的一些基本特性:

  • LinkedList 实现了 List 接口,并继承了 AbstractSequentialList ,它支持所有 List 的操作。
  • LinkedList 实现了 Deque 接口,也可以被当作队列(Queue)或双端队列(Deque)进行操作,此外,也可以用来实现栈。
  • LinkedList 实现了 Cloneable 接口,默认为浅拷贝
  • LinkedList 实现了 Serializable 接口,支持序列化
  • LinkedList非线程安全的。

LinkedList 的数据结构

LinkedList 内部维护了一个双链表

LinkedList 通过 Node 类型的头尾指针(firstlast)来访问数据。

1
2
3
4
5
6
// 链表长度
transient int size = 0;
// 链表头节点
transient Node<E> first;
// 链表尾节点
transient Node<E> last;
  • size - 表示双链表中节点的个数,初始为 0
  • firstlast - 分别是双链表的头节点和尾节点

NodeLinkedList 的内部类,它表示链表中的元素实例。Node 中包含三个元素:

  • prev 是该节点的上一个节点;
  • next 是该节点的下一个节点;
  • item 是该节点所包含的值。
1
2
3
4
5
6
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
...
}

LinkedList 的序列化

LinkedListArrayList 一样也定制了自身的序列化方式。具体做法是:

  • size (双链表容量大小)、firstlast (双链表的头尾节点)修饰为 transient,使得它们可以被 Java 序列化所忽略。
  • 重写了 writeObject()readObject() 来控制序列化时,只处理双链表中能被头节点链式引用的节点元素。

LinkedList 访问元素

LinkedList 访问元素的实现主要基于以下关键性源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

Node<E> node(int index) {
// assert isElementIndex(index);

if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

获取 LinkedList 第 index 个元素的算法是:

  • 判断 index 在链表前半部分,还是后半部分。
  • 如果是前半部分,从头节点开始查找;如果是后半部分,从尾结点开始查找。

LinkedList 这种访问元素的性能是 O(N) 级别的(极端情况下,扫描 N/2 个元素);相比于 ArrayListO(1),显然要慢不少。

推荐使用迭代器遍历 LinkedList ,不要使用传统的 for 循环。注:foreach 语法会被编译器转换成迭代器遍历,但是它的遍历过程中不允许修改 List 长度,即不能进行增删操作。

LinkedList 添加元素

LinkedList 有多种添加元素方法:

  • add(E e):默认添加元素方法(插入尾部)
  • add(int index, E element):添加元素到任意位置
  • addFirst(E e):在头部添加元素
  • addLast(E e):在尾部添加元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean add(E e) {
linkLast(e);
return true;
}

public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

public void addFirst(E e) {
linkFirst(e);
}

public void addLast(E e) {
linkLast(e);
}

LinkedList 添加元素的实现主要基于以下关键性源码:

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
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}

算法如下:

  • 将新添加的数据包装为 Node
  • 如果往头部添加元素,将头指针 first 指向新的 Node,之前的 first 对象的 prev 指向新的 Node
  • 如果是向尾部添加元素,则将尾指针 last 指向新的 Node,之前的 last 对象的 next 指向新的 Node

LinkedList 删除元素

LinkedList 删除元素的实现主要基于以下关键性源码:

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
public boolean remove(Object o) {
if (o == null) {
// 遍历找到要删除的元素节点
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 遍历找到要删除的元素节点
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;

if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}

if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}

x.item = null;
size--;
modCount++;
return element;
}

算法说明:

  • 遍历找到要删除的元素节点,然后调用 unlink 方法删除节点;
  • unlink 删除节点的方法:
    • 如果当前节点有前驱节点,则让前驱节点指向当前节点的下一个节点;否则,让双链表头指针指向下一个节点。
    • 如果当前节点有后继节点,则让后继节点指向当前节点的前一个节点;否则,让双链表尾指针指向上一个节点。

ArrayList vs. LinkedList

  • 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  • 底层数据结构: ArrayList 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
  • 插入和删除是否受元素位置的影响:
    • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
    • LinkedList 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(add(E e)addFirst(E e)addLast(E e)removeFirst()removeLast()),时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element)remove(Object o),remove(int index)), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。
  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList(实现了 RandomAccess 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

我们在项目中一般是不会使用到 LinkedList 的,需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且,性能通常会更好!就连 LinkedList 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 LinkedList

List 常见问题

Arrays.asList 问题点

在业务开发中,我们常常会把原始的数组转换为 List 类数据结构,来继续展开各种 Stream 操作。通常,我们会使用 Arrays.asList 方法可以把数组一键转换为 List

【示例】Arrays.asList 转换基本类型数组

1
2
3
int[] arr = { 1, 2, 3 };
List list = Arrays.asList(arr);
log.info("list:{} size:{} class:{}", list, list.size(), list.get(0).getClass());

【输出】

1
11:26:33.214 [main] INFO io.github.dunwu.javacore.container.list.AsList示例 - list:[[I@ae45eb6] size:1 class:class [I

数组元素个数为 3,但转换后的列表个数为 1。

由此可知, Arrays.asList 第一个问题点:不能直接使用 Arrays.asList 来转换基本类型数组

其原因是:Arrays.asList 方法传入的是一个泛型 T 类型可变参数,最终 int 数组整体作为了一个对象成为了泛型类型 T:

1
2
3
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

直接遍历这样的 List 必然会出现 Bug,修复方式有两种,如果使用 Java8 以上版本可以使用 Arrays.stream 方法来转换,否则可以把 int 数组声明为包装类型 Integer 数组:

【示例】转换整型数组为 List 的正确方式

1
2
3
4
5
6
7
int[] arr1 = { 1, 2, 3 };
List list1 = Arrays.stream(arr1).boxed().collect(Collectors.toList());
log.info("list:{} size:{} class:{}", list1, list1.size(), list1.get(0).getClass());

Integer[] arr2 = { 1, 2, 3 };
List list2 = Arrays.asList(arr2);
log.info("list:{} size:{} class:{}", list2, list2.size(), list2.get(0).getClass());

【示例】Arrays.asList 转换引用类型数组

1
2
3
4
5
6
7
8
9
String[] arr = { "1", "2", "3" };
List list = Arrays.asList(arr);
arr[1] = "4";
try {
list.add("5");
} catch (Exception ex) {
ex.printStackTrace();
}
log.info("arr:{} list:{}", Arrays.toString(arr), list);

抛出 java.lang.UnsupportedOperationException

抛出异常的原因在于 Arrays.asList 第二个问题点:**Arrays.asList 返回的 List 不支持增删操作**。Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类 ArrayList

查看源码,我们可以发现 Arrays.asList 返回的 ArrayList 继承了 AbstractList,但是并没有覆写 addremove 方法。

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
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;

ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}

// ...

@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}

}

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
public void add(int index, E element) {
throw new UnsupportedOperationException();
}

public E remove(int index) {
throw new UnsupportedOperationException();
}
}

Arrays.asList 第三个问题点:**对原始数组的修改会影响到我们获得的那个 List**。ArrayList 其实是直接使用了原始的数组。

解决方法很简单,重新 new 一个 ArrayList 初始化 Arrays.asList 返回的 List 即可:

1
2
3
4
5
6
7
8
9
String[] arr = { "1", "2", "3" };
List list = new ArrayList(Arrays.asList(arr));
arr[1] = "4";
try {
list.add("5");
} catch (Exception ex) {
ex.printStackTrace();
}
log.info("arr:{} list:{}", Arrays.toString(arr), list);

List.subList 问题点

List.subList 直接引用了原始的 List,也可以认为是共享“存储”,而且对原始 List 直接进行结构性修改会导致 SubList 出现异常。

1
2
3
4
5
6
7
8
private static List<List<Integer>> data = new ArrayList<>();

private static void oom() {
for (int i = 0; i < 1000; i++) {
List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());
data.add(rawList.subList(0, 1));
}
}

出现 OOM 的原因是,循环中的 1000 个具有 10 万个元素的 List 始终得不到回收,因为它始终被 subList 方法返回的 List 强引用。

解决方法是:

1
2
3
4
5
6
private static void oomfix() {
for (int i = 0; i < 1000; i++) {
List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());
data.add(new ArrayList<>(rawList.subList(0, 1)));
}
}

【示例】子 List 强引用原始的 List

1
2
3
4
5
6
7
8
9
10
11
12
13
private static void wrong() {
List<Integer> list = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList());
List<Integer> subList = list.subList(1, 4);
System.out.println(subList);
subList.remove(1);
System.out.println(list);
list.add(0);
try {
subList.forEach(System.out::println);
} catch (Exception ex) {
ex.printStackTrace();
}
}

抛出 java.util.ConcurrentModificationException

解决方法:

一种是,不直接使用 subList 方法返回的 SubList,而是重新使用 new ArrayList,在构造方法传入 SubList,来构建一个独立的 ArrayList;

另一种是,对于 Java 8 使用 Stream 的 skip 和 limit API 来跳过流中的元素,以及限制流中元素的个数,同样可以达到 SubList 切片的目的。

1
2
3
4
//方式一:
List<Integer> subList = new ArrayList<>(list.subList(1, 4));
//方式二:
List<Integer> subList = list.stream().skip(1).limit(3).collect(Collectors.toList());

参考资料

SQL

::: info 概述

SQL(Structured Query Language,结构化查询语言) 是一种高级的非过程化编程语言,用于管理 RDBMS(Relational Database Management System,关系数据库管理系统)

本文主要介绍关系型数据库的基本语法,限于篇幅,本文侧重说明用法,不会展开讲解特性、原理。

注:本文语法主要针对 Mysql,但大部分的语法对其他关系型数据库也适用。

:::

SQL 简介

数据库术语

  • 数据库(database) - 保存有组织的数据的容器(通常是一个文件或一组文件)。
  • 数据表(table) - 某种特定类型数据的结构化清单。
  • 模式(schema) - 关于数据库和表的布局及特性的信息。模式定义了数据在表中如何存储,包含存储什么样的数据,数据如何分解,各部分信息如何命名等信息。数据库和表都有模式。
  • 行(row) - 表中的一条记录。
  • 列(column) - 表中的一个字段。所有表都是由一个或多个列组成的。
  • 主键(primary key) - 一列(或一组列),其值能够唯一标识表中每一行。

SQL 语法

SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。

SQL 语法结构

img

SQL 语法结构包括:

  • 子句 - 是语句和查询的组成成分(在某些情况下,这些都是可选的)。
  • 表达式 - 可以产生任何标量值,或由列和行的数据库表。
  • 谓词 - 给需要评估的 SQL 三值逻辑(3VL)(true/false/unknown)或布尔真值指定条件,并限制语句和查询的效果,或改变程序流程。
  • 查询 - 基于特定条件检索数据。这是 SQL 的一个重要组成部分。
  • 语句 - 可以持久地影响纲要和数据,也可以控制数据库事务、程序流程、连接、会话或诊断。

SQL 语法要点

  • SQL 语句不区分大小写,但是数据库表名、列名和值是否区分,依赖于具体的 DBMS 以及配置。

例如:SELECTselectSelect 是相同的。

  • 多条 SQL 语句必须以分号(;)分隔

  • 处理 SQL 语句时,所有空格都被忽略。SQL 语句可以写成一行,也可以分写为多行。

1
2
3
4
5
6
7
-- 一行 SQL 语句
UPDATE user SET username='robot', password='robot' WHERE username = 'root';

-- 多行 SQL 语句
UPDATE user
SET username='robot', password='robot'
WHERE username = 'root';
  • SQL 支持三种注释
1
2
3
4
5
6
7
8
9
10
11
SELECT prod_name -- 这是一条注释
FROM Products;

# 这是一条注释
SELECT prod_name
FROM Products;

/* SELECT prod_name, vend_id
FROM Products; */
SELECT prod_name
FROM Products;

SQL 分类

  • DDL - DDL,英文叫做 Data Definition Language,即“数据定义语言”
    • DDL 用于定义数据库对象
    • DDL 定义操作包括创建(CREATE)、删除(DROP)、修改(ALTER);而被操作的对象包括:数据库、数据表和列、视图、索引。
  • DML - DML,英文叫做 Data Manipulation Language,即“数据操作语言”
    • DML 用于访问数据库的数据
    • DML 访问操作包括插入(INSERT)、删除(DELETE)、修改(UPDATE)、查询(SELECT)。这四个指令合称 CRUD,英文单词为 Create, Read, Update, Delete,即增删改查。
  • TCL - TCL,英文叫做 Transaction Control Language,即“事务控制语言”
    • TCL 用于管理数据库中的事务,实际上就是用于管理由 DML 语句所产生的数据变更,它还允许将语句分组为逻辑事务。
    • TCL 的核心指令是 COMMITROLLBACK
  • DCL - DCL,英文叫做 Data Control Language,即“数据控制语言”
    • DCL 用于对数据访问权限进行控制,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。
    • DCL 的核心指令是 GRANTREVOKE
    • DCL 以控制用户的访问权限为主,因此其指令作法并不复杂,可利用 DCL 控制的权限有:CONNECTSELECTINSERTUPDATEDELETEEXECUTEUSAGEREFERENCES
    • 根据不同的 DBMS 以及不同的安全性实体,其支持的权限控制也有所不同。

数据定义(CREATE、ALTER、DROP)

DDL 的主要功能是定义数据库对象(如:数据库、数据表、视图、索引等)。

数据库(DATABASE)

以下为数据库定义示例:

::: tabs#数据库定义

@tab 创建数据库

1
CREATE DATABASE IF NOT EXISTS db_tutorial;

@tab 删除数据库

1
DROP DATABASE IF EXISTS db_tutorial;

@tab 选择数据库

1
USE db_tutorial;

:::

数据表(TABLE)

以下为数据表定义示例:

::: tabs#数据表定义

@tab 创建数据表

利用 CREATE TABLE 创建表,必须给出下列信息:

  • 新表的名字,在关键字 CREATE TABLE 之后给出;
  • 表列的名字和定义,用逗号分隔;
  • 有的 DBMS 还要求指定表的位置。
1
2
3
4
5
6
CREATE TABLE user (
id INT(10) UNSIGNED NOT NULL COMMENT 'Id',
username VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '用户名',
password VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '密码',
email VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '邮箱'
) COMMENT ='用户表';

@tab 删除数据表

1
2
DROP TABLE IF EXISTS user;
DROP TABLE CustCopy;

@tab 复制表

1
2
CREATE TABLE vip_user AS
SELECT * FROM user;

@tab 数据表添加列

1
2
ALTER TABLE user
ADD age int(3);

@tab 数据表删除列

1
2
ALTER TABLE user
DROP COLUMN age;

@tab 数据表修改列

1
2
ALTER TABLE user
MODIFY COLUMN age tinyint;

@tab 修改表的编码格式

utf8mb4 编码是 utf8 编码的超集,兼容 utf8,并且能存储 4 字节的表情字符。如果表的编码指定为 utf8,在保存 emoji 字段时会报错。

1
ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

:::

以下为数据表信息查看示例:

::: tabs#数据表查看

@tab 查看表的基本信息

1
2
SELECT * FROM information_schema.tables
WHERE table_schema = 'test' AND table_name = 'user';

@tab 查看表的列信息

1
2
SELECT * FROM information_schema.columns
WHERE table_schema = 'test' AND table_name = 'user';

:::

视图(VIEW)

“视图”是基于 SQL 语句的结果集的可视化的表。视图是虚拟的表,本身不存储数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。

视图的作用:

  • 简化复杂的 SQL 操作,比如复杂的连接。
  • 只使用实际表的一部分数据。
  • 通过只给用户访问视图的权限,保证数据的安全性。
  • 更改数据格式和表示。

以下为视图定义示例:

::: tabs#视图定义

@tab 创建视图

创建一个名为 ProductCustomers 的视图,它联结三个表,返回已订购了任意产品的所有顾客的列表。

1
2
3
4
5
CREATE VIEW ProductCustomers AS
SELECT cust_name, cust_contact, prod_id
FROM Customers, Orders, OrderItems
WHERE Customers.cust_id = Orders.cust_id
AND OrderItems.order_num = Orders.order_num;

检索订购了产品 RGAN01 的顾客

1
2
3
SELECT cust_name, cust_contact
FROM ProductCustomers
WHERE prod_id = 'RGAN01';

@tab 删除视图

1
DROP VIEW top_10_user_view;

:::

索引(INDEX)

“索引”是数据库为了提高查找效率的一种数据结构

日常生活中,我们可以通过检索目录,来快速定位书本中的内容。索引和数据表,就好比目录和书,想要高效查询数据表,索引至关重要。在数据量小且负载较低时,不恰当的索引对于性能的影响可能还不明显;但随着数据量逐渐增大,性能则会急剧下降。因此,设置合理的索引是数据库查询性能优化的最有效手段

更新一个包含索引的表需要比更新一个没有索引的表花费更多的时间,这是由于索引本身也需要更新。因此,理想的做法是仅仅在常常被搜索的列(以及表)上面创建索引。

“唯一索引”表明此索引的每一个索引值只对应唯一的数据记录。

以下为视图定义示例:

::: tabs#索引定义

@tab 创建索引

1
CREATE INDEX idx_email ON user(email);

@tab 创建唯一索引

1
CREATE UNIQUE INDEX uniq_name ON user(name);

@tab 删除索引

1
2
ALTER TABLE user DROP INDEX idx_email;
ALTER TABLE user DROP INDEX uniq_name;

@tab 添加主键

1
ALTER TABLE user ADD PRIMARY KEY (id);

@tab 删除主键

1
ALTER TABLE user DROP PRIMARY KEY;

:::

约束(CONSTRAINT)

约束(constraint)管理如何插入或处理数据库数据的规则。

如果存在违反约束的数据行为,行为会被约束终止。约束可以在创建表时规定(通过 CREATE TABLE 语句),或者在表创建之后规定(通过 ALTER TABLE 语句)。

定义约束的语法:

1
2
3
4
5
6
CREATE TABLE table_name (
column_name1 data_type(size) constraint_name,
column_name2 data_type(size) constraint_name,
column_name3 data_type(size) constraint_name,
....
);

约束类型

  • NOT NULL - 指示字段不能存储 NULL 值。
  • UNIQUE KEY - 保证字段的每行必须有唯一的值。
  • PRIMARY KEY - PRIMARY KEY 的作用是唯一标识一条记录,不能重复,不能为空,即相当于 NOT NULL + UNIQUE。确保字段(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。
  • FOREIGN KEY - 保证一个表中的数据匹配另一个表中的值的参照完整性。
  • CHECK - 用于检查字段取值范围的有效性。
  • DEFAULT - 表明字段的默认值。如果插入数据时,该字段没有赋值,就会被设置为默认值。

以下为约束定义示例:

::: tabs#约束定义

@tab NOT NULL

1
2
3
CREATE TABLE demo (
id INT UNSIGNED NOT NULL
);

@tab UNIQUE KEY

1
2
3
4
CREATE TABLE demo2 (
id INT UNSIGNED NOT NULL,
name VARCHAR(50) NOT NULL UNIQUE KEY
);

@tab PRIMARY KEY

1
2
3
4
CREATE TABLE demo3 (
id INT UNSIGNED NOT NULL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE KEY
);

@tab FOREIGN KEY

1
2
3
4
5
6
CREATE TABLE demo4 (
id INT UNSIGNED NOT NULL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE KEY,
fid INT UNSIGNED,
FOREIGN KEY (fid) REFERENCES demo3(id)
);

@tab CHECK

1
2
3
4
5
CREATE TABLE demo5 (
id INT UNSIGNED NOT NULL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE KEY,
age INT CHECK (age > 0)
);

@tab DEFAULT

1
2
3
4
5
CREATE TABLE demo6 (
id INT UNSIGNED NOT NULL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE KEY,
age INT DEFAULT 0
);

:::

增删改查(CRUD)

增删改查,又称为 **CRUD**,是数据库基本操作中的基本操作。

插入数据(INSERT)

INSERT INTO 语句用于向表中插入新记录。

以下为插入数据示例:

::: tabs#插入数据

@tab 插入完整的行

1
2
3
4
5
6
-- 下面两条 SQL 等价
INSERT INTO Customers
VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY', '11111', 'USA', NULL, NULL);

INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email)
VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY','11111', 'USA', NULL, NULL);

@tab 插入行的一部分

1
2
INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country)
VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY', '11111', 'USA');

@tab 插入查询出来的数据

1
2
3
INSERT INTO Customers(cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country)
SELECT cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country
FROM CustNew;

@tab 从一个表复制到另一个表

1
2
3
4
5
6
7
SELECT *
INTO CustCopy
FROM Customers;

-- MariaDB、MySQL、Oracle、PostgreSQL 和 SQLite
CREATE TABLE CustCopy AS
SELECT * FROM Customers;

:::

更新数据(UPDATE)

UPDATE 语句用于更新表中的记录。

::: tabs#更新数据

@tab 更新单列

更新客户 1000000005 的电子邮件地址

1
2
3
UPDATE Customers
SET cust_email = 'kim@thetoystore.com'
WHERE cust_id = '1000000005';

@tab 更新多列

1
2
3
UPDATE customers
SET cust_contact = 'Sam Roberts', cust_email = 'sam@toyland.com'
WHERE cust_id = '1000000006';

@tab 从表中删除特定的行

1
2
DELETE FROM Customers
WHERE cust_id = '1000000006';

:::

删除数据(DELETE)

  • DELETE 语句用于删除表中的记录。
  • TRUNCATE TABLE 可以清空表,也就是删除所有行。

以下为删除数据示例:

::: tabs#删除数据

@tab 删除表中的指定数据

1
DELETE FROM user WHERE username = 'robot';

@tab 清空表中的数据

1
TRUNCATE TABLE user;

@tab 批量删除大量数据

如果要根据时间范围批量删除大量数据,最简单的语句如下:

1
2
DELETE FROM order
WHERE timestamp < SUBDATE(CURDATE(), INTERVAL 3 MONTH);

上面的语句,大概率执行会报错,提示删除失败,因为需要删除的数据量太大了,所以需要分批删除。

可以先通过一次查询,找到符合条件的历史订单中最大的那个订单 ID,然后在删除语句中把删除的条件转换成按主键删除。

1
2
3
4
5
6
SELECT max(id) FROM order
WHERE timestamp < SUBDATE(CURDATE(), INTERVAL 3 MONTH);

-- 分批删除,? 填上一条语句查到的最大 ID
DELETE FROM order
WHERE id <= ? ORDER BY id LIMIT 1000;

:::

查询数据(SELECT)

  • SELECT 语句用于从数据库中查询数据。
  • DISTINCT 用于返回唯一不同的值。它作用于所有列,也就是说所有列的值都相同才算相同。
  • LIMIT 限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
    • ASC :升序(默认)
    • DESC :降序

SELECT 的用法

以下为查询数据示例:

::: tabs#删除数据

@tab 查询单列

1
2
SELECT prod_name
FROM Products;

@tab 查询多列

1
2
SELECT prod_id, prod_name, prod_price
FROM Products;

@tab 查询所有列

1
2
SELECT *
FROM Products;

@tab 查询去重

1
2
SELECT DISTINCT vend_id
FROM Products;

@tab 限制查询数量

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
-- SQL Server 和 Access
SELECT TOP 5 prod_name
FROM Products;

-- DB2
SELECT prod_name
FROM Products
FETCH FIRST 5 ROWS ONLY;

-- Oracle
SELECT prod_name
FROM Products
WHERE ROWNUM <=5;

-- MySQL、MariaDB、PostgreSQL 或者 SQLite
SELECT prod_name
FROM Products
LIMIT 5;
-- 检索从第 5 行起的 5 行数据
SELECT prod_name
FROM Products
LIMIT 5 OFFSET 5;
-- MySQL 和 MariaDB 中,上面的示例可以简化如下
SELECT prod_name
FROM Products
LIMIT 5, 5;

:::

SELECT 的执行顺序

关键字的顺序是不能颠倒的:

1
SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ...

SELECT 语句的执行顺序(在 MySQL 和 Oracle 中,SELECT 执行顺序基本相同):

1
FROM > WHERE > GROUP BY > HAVING > SELECT 的字段 > DISTINCT > ORDER BY > LIMIT

比如你写了一个 SQL 语句,那么它的关键字顺序和执行顺序是下面这样的:

1
2
3
4
5
6
7
SELECT DISTINCT player_id, player_name, count(*) as num -- 顺序 5
FROM player JOIN team ON player.team_id = team.team_id -- 顺序 1
WHERE height > 1.80 -- 顺序 2
GROUP BY player.team_id -- 顺序 3
HAVING num > 2 -- 顺序 4
ORDER BY num DESC -- 顺序 6
LIMIT 2 -- 顺序 7

过滤数据(WHERE)

数据库表一般包含大量的数据,很少需要检索表中的所有行。通常只会根据特定操作或报告的需要提取表数据的子集。只检索所需数据需要指 定搜索条件(search criteria),搜索条件也称为过滤条件(filter condition)。

WHERE

在 SQL 语句中,数据根据 WHERE 子句中指定的搜索条件进行过滤。

WHERE 子句的基本格式如下:

1
SELECT ……(列名) FROM ……(表名) WHERE ……(子句条件)

WHERE 的常见用法:

1
2
3
4
5
6
SELECT column1, column2 FROM table_name WHERE condition;
SELECT * FROM table_name WHERE condition1 AND condition2;
SELECT * FROM table_name WHERE condition1 OR condition2;
SELECT * FROM table_name WHERE NOT condition;
SELECT * FROM table_name WHERE condition1 AND (condition2 OR condition3);
SELECT * FROM table_name WHERE EXISTS (SELECT column_name FROM table_name WHERE condition)

WHERE 可以与 SELECTUPDATEDELETE 一起使用。

::: tabs#WHERE 示例

@tab SELECT 语句中的 WHERE 子句

检索所有价格小于 10 美元的产品。

1
2
3
SELECT prod_name, prod_price
FROM Products
WHERE prod_price < 10;

检索所有不是供应商 DLL01 制造的产品

1
2
3
4
5
6
7
8
9
-- 下面两条查询语句作用相同

SELECT vend_id, prod_name
FROM Products
WHERE vend_id <> 'DLL01';

SELECT vend_id, prod_name
FROM Products
WHERE vend_id != 'DLL01';

检索价格在 5 美元和 10 美元之间的所有产品

1
2
3
SELECT prod_name, prod_price
FROM Products
WHERE prod_price BETWEEN 5 AND 10;

检索所有没有邮件地址的顾客

1
2
3
SELECT cust_name
FROM CUSTOMERS
WHERE cust_email IS NULL;

@tab UPDATE 语句中的 WHERE 子句

1
2
3
UPDATE Customers
SET cust_name = 'Jack Jones'
WHERE cust_name = 'Kids Place';

@tab DELETE 语句中的 WHERE 子句

1
2
DELETE FROM Customers
WHERE cust_name = 'Kids Place';

:::

比较操作符

操作符 描述
= 等于
<> 不等于。注释:在 SQL 的一些版本中,该操作符可被写成 !=
> 大于
< 小于
>= 大于等于
<= 小于等于
IS NULL 是否为空

【示例】查询所有价格小于 10 美元的产品

1
2
3
SELECT prod_name, prod_price
FROM Products
WHERE prod_price < 10;

【示例】查询所有不是供应商 DLL01 制造的产品

1
2
3
SELECT vend_id, prod_name
FROM Products
WHERE vend_id != 'DLL01';

【示例】查询邮件地址为空的客户

1
2
3
SELECT cust_name
FROM CUSTOMERS
WHERE cust_email IS NULL;

范围操作符

操作符 描述
BETWEEN 在某个范围内
IN 指定针对某个列的多个可能值

BETWEEN 操作符在 WHERE 子句中使用,作用是选取介于某个范围内的值。

IN 操作符用来指定条件范围,范围中的每个条件都可以进行匹配。IN 取一组由逗号分隔、括在圆括号中的合法值。

为什么要使用 IN 操作符?其优点如下。

  • 在有很多合法选项时,IN 操作符的语法更清楚,更直观。
  • 在与其他 AND 和 OR 操作符组合使用 IN 时,求值顺序更容易管理。
  • IN 操作符一般比一组 OR 操作符执行得更快(在上面这个合法选项很 少的例子中,你看不出性能差异)。
  • IN 的最大优点是可以包含其他 SELECT 语句,能够更动态地建立 WHERE 子句。

以下为范围操作符使用示例:

::: tabs#范围操作符

@tab IN 示例

下面两条 SQL 的语义等价:

1
2
3
4
5
6
7
8
9
SELECT prod_name, prod_price
FROM Products
WHERE vend_id IN ( 'DLL01', 'BRS01' )
ORDER BY prod_name;

SELECT prod_name, prod_price
FROM Products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'
ORDER BY prod_name;

@tab BETWEEN 示例

1
2
3
SELECT prod_name, prod_price
FROM Products
WHERE prod_price BETWEEN 5 AND 10;

:::

逻辑操作符

操作符 描述
AND 并且(与)
OR 或者(或)
NOT 否定(非)

ANDORNOT 是用于对过滤条件的逻辑处理指令。

  • AND 优先级高于 OR,为了明确处理顺序,可以使用 ()AND 操作符表示左右条件都要满足。

  • OR 操作符表示左右条件满足任意一个即可。

  • NOT 操作符用于否定其后条件。

以下为逻辑操作符使用示例:

::: tabs#逻辑操作符

@tab AND 示例

检索由供应商 DLL01 制造且价格小于等于 4 美元的所有产品的名称和价格

1
2
3
SELECT prod_id, prod_price, prod_name
FROM Products
WHERE vend_id = 'DLL01' AND prod_price <= 4;

@tab OR 示例

检索由供应商 DLL01 或供应商 BRS01 制造的所有产品的名称和价格

1
2
3
SELECT prod_id, prod_price, prod_name
FROM Products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01';

@tab NOT 示例

检索除 DLL01 之外的所有供应商制造的产品

1
2
3
4
SELECT prod_name
FROM Products
WHERE NOT vend_id = 'DLL01'
ORDER BY prod_name;

和下面的示例作用相同

1
2
3
4
SELECT prod_name
FROM Products
WHERE vend_id <> 'DLL01'
ORDER BY prod_name;

@tab ANDOR 优先级示例

SQL 在处理 OR 操作符前,优先处理 AND 操作符。

下面的示例中,SQL 会理解为由供应商 BRS01 制造的价格为 10 美元以上的所有产品,以及由供应商 DLL01 制造的所有产品,而不管其价格如何。

1
2
3
4
SELECT prod_name, prod_price
FROM Products
WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'
AND prod_price >= 10;

任何时候使用具有 AND 和 OR 操作符的 WHERE 子句,都应该使用圆括号明确地分组操作符。

1
2
3
4
SELECT prod_name, prod_price
FROM Products
WHERE (vend_id = 'DLL01' OR vend_id = 'BRS01')
AND prod_price >= 10;

:::

通配符

LIKE 操作符在 WHERE 子句中使用,作用是确定字符串是否匹配模式。只有字段是文本值时才使用 LIKE不要滥用通配符,通配符位于开头处匹配会非常慢

LIKE 支持以下通配符匹配选项:

  • % 表示任何字符出现任意次数。
  • _ 表示任何字符出现一次。
  • [] 必须匹配指定位置的一个字符。

说明:并不是所有 DBMS 都支持 []。只有微软的 Access 和 SQL Server 支持 []

以下为通配符使用示例:

::: tabs#逻辑操作符

@tab % 示例

检索所有产品名以 Fish 开头的产品

1
2
3
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE 'Fish%';

检索产品名中包含 bean bag 的产品

1
2
3
SELECT prod_id, prod_name
FROM Products
WHERE prod_name LIKE '%bean bag%';

检索产品名中以 F 开头,y 结尾的产品

1
2
3
SELECT prod_name
FROM Products
WHERE prod_name LIKE 'F%y';

@tab _ 示例

1
2
SELECT * FROM Products
WHERE prod_name LIKE '__ inch teddy bear';

@tab [] 示例

找出所有名字以 J 或 M 开头的联系人:

1
2
3
4
SELECT cust_contact
FROM Customers
WHERE cust_contact LIKE '[JM]%'
ORDER BY cust_contact;

:::

子查询

子查询(subquery),即嵌套在其他查询中的查询。

子查询可以分为关联子查询和非关联子查询。

  • 子查询从数据表中查询了数据结果,如果这个数据结果只执行一次,然后这个数据结果作为主查询的条件进行执行,那么这样的子查询叫做非关联子查询

  • 如果子查询需要执行多次,即采用循环的方式,先从外部查询开始,每次都传入子查询进行查询,然后再将结果反馈给外部,这种嵌套的执行方式就称为关联子查询

假如需要列出订购物品 RGAN01 的所有顾客,应该怎样检索?下面列出具体的步骤。

(1) 检索包含物品 RGAN01 的所有订单的编号。

1
2
3
SELECT order_num
FROM OrderItems
WHERE prod_id = 'RGAN01';

输出

1
2
3
4
order_num
-----------
20007
20008

(2) 检索具有前一步骤列出的订单编号的所有顾客的 ID。

1
2
3
SELECT cust_id
FROM Orders
WHERE order_num IN (20007,20008);

输出

1
2
3
4
cust_id
----------
1000000004
1000000005

(3) 检索前一步骤返回的所有顾客 ID 的顾客信息。

1
2
3
SELECT cust_name, cust_contact
FROM Customers
WHERE cust_id IN ('1000000004','1000000005');

现在,结合这两个查询,把第一个查询(返回订单号的那一个)变为子查询。

1
2
3
4
5
SELECT cust_id
FROM orders
WHERE order_num IN (SELECT order_num
FROM orderitems
WHERE prod_id = 'RGAN01');

再进一步结合第三个查询

1
2
3
4
5
6
7
SELECT cust_name, cust_contact
FROM customers
WHERE cust_id IN (SELECT cust_id
FROM orders
WHERE order_num IN (SELECT order_num
FROM orderitems
WHERE prod_id = 'RGAN01'));

联结和组合

联结(JOIN)

**在 SELECT, UPDATE 和 DELETE 语句中,“联结”可以用于联合多表查询。联结使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE**。

联结可以替换子查询,并且一般比子查询的效率更快

JOIN 有以下类型:

  • 内联结 - 内联结又称等值联结,用于获取两个表中字段匹配关系的记录,使用 INNER JOIN 关键字。在没有条件语句的情况下返回笛卡尔积
    • 笛卡尔积 - “笛卡尔积”也称为交叉联结(CROSS JOIN)。由没有联结条件的表关系返回的结果为笛卡儿积。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。
    • 自联结(=) - “自联结(=)”可以看成内联结的一种,只是联结的表是自身而已。
    • 自然联结(NATURAL JOIN) - “自然联结”会自动联结所有同名列。自然联结使用 NATURAL JOIN 关键字。
  • 外联结
    • 左联结(LEFT JOIN) - “左外联结”会获取左表所有记录,即使右表没有对应匹配的记录。左外联结使用 LEFT JOIN 关键字。
    • 右联结(RIGHT JOIN) - “右外联结”会获取右表所有记录,即使左表没有对应匹配的记录。右外联结使用 RIGHT JOIN 关键字。

SQL JOIN

内联结(INNER JOIN)

内联结又称等值联结,用于获取两个表中字段匹配关系的记录,使用 INNER JOIN 关键字。在没有条件语句的情况下返回笛卡尔积

1
2
3
4
5
6
7
8
SELECT vend_name, prod_name, prod_price
FROM vendors INNER JOIN products
ON vendors.vend_id = products.vend_id;

-- 也可以省略 INNER 使用 JOIN,与上面一句效果一样
SELECT vend_name, prod_name, prod_price
FROM vendors JOIN products
ON vendors.vend_id = products.vend_id;
笛卡尔积

“笛卡尔积”也称为交叉联结(CROSS JOIN),它的作用就是可以把任意表进行联结,即使这两张表不相关。但通常进行联结还是需要筛选的,因此需要在联结后面加上 WHERE 子句,也就是作为过滤条件对联结数据进行筛选。

笛卡尔积是一个数学运算。假设我有两个集合 X 和 Y,那么 X 和 Y 的笛卡尔积就是 X 和 Y 的所有可能组合,也就是第一个对象来自于 X,第二个对象来自于 Y 的所有可能。

【示例】求 t1 和 t2 两张表的笛卡尔积

1
2
3
-- 以下两条 SQL,执行结果相同
SELECT * FROM t1, t2;
SELECT * FROM t1 CROSS JOIN t2;
自联结(=)

“自联结”可以看成内联结的一种,只是联结的表是自身而已

给与 Jim Jones 同一公司的所有顾客发送一封信件:

1
2
3
4
5
6
7
8
9
10
11
-- 子查询方式
SELECT cust_id, cust_name, cust_contact
FROM customers
WHERE cust_name = (SELECT cust_name
FROM customers
WHERE cust_contact = 'Jim Jones');

-- 自联结方式
SELECT c1.cust_id, c1.cust_name, c1.cust_contact
FROM customers AS c1, customers AS c2
WHERE c1.cust_name = c2.cust_name AND c2.cust_contact = 'Jim Jones';
自然联结(NATURAL JOIN)

“自然联结”会自动联结所有同名列。自然联结使用 NATURAL JOIN 关键字。

1
2
3
SELECT *
FROM Products
NATURAL JOIN Customers;

外联结(OUTER JOIN)

外联结返回一个表中的所有行,并且仅返回来自此表中满足联结条件的那些行,即两个表中的列是相等的。外联结分为左外联结、右外联结、全外联结(Mysql 不支持)。

左联结(LEFT JOIN)

“左外联结”会获取左表所有记录,即使右表没有对应匹配的记录。左外联结使用 LEFT JOIN 关键字。

1
2
3
SELECT customers.cust_id, orders.order_num
FROM customers LEFT JOIN orders
ON customers.cust_id = orders.cust_id;
右联结(RIGHT JOIN)

“右外联结”会获取右表所有记录,即使左表没有对应匹配的记录。右外联结使用 RIGHT JOIN 关键字。

1
2
3
SELECT customers.cust_id, orders.order_num
FROM customers RIGHT JOIN orders
ON customers.cust_id = orders.cust_id;

组合(UNION)

UNION 运算符将两个或更多查询的结果组合起来,并生成一个结果集,其中包含来自 UNION 中参与查询的提取行。

UNION 基本规则:

  • 所有查询的列数和列顺序必须相同。
  • 每个查询中涉及表的列的数据类型必须相同或兼容。
  • 通常返回的列名取自第一个查询。

主要有两种情况需要使用组合查询:

  • 在一个查询中从不同的表返回结构数据;
  • 对一个表执行多个查询,按一个查询返回数据。

把 Illinois、Indiana、Michigan 等州的缩写传递给 IN 子句,检索出这些州的所有行

1
2
3
SELECT cust_name, cust_contact, cust_email
FROM Customers
WHERE cust_state IN ('IL','IN','MI');

找出所有 Fun4All

1
2
3
SELECT cust_name, cust_contact, cust_email
FROM Customers
WHERE cust_name = 'Fun4All';

组合这两条语句

1
2
3
4
5
6
7
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_state IN ('IL', 'IN', 'MI')
UNION
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_name = 'Fun4All';

UNION 默认从查询结果集中自动去除了重复的行;如果想返回所有的匹配行,可使用 UNION ALL

1
2
3
4
5
6
7
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_state IN ('IL', 'IN', 'MI')
UNION ALL
SELECT cust_name, cust_contact, cust_email
FROM customers
WHERE cust_name = 'Fun4All';

JOIN vs UNION

  • JOIN 中联结表的列可能不同,但在 UNION 中,所有查询的列数和列顺序必须相同。
  • UNION 将查询之后的行放在一起(垂直放置),但 JOIN 将查询之后的列放在一起(水平放置),即它构成一个笛卡尔积。

排序和分组

ORDER BY

ORDER BY 用于对结果集进行排序。ORDER BY 子句取一个或多个列的名字,据此对输出进行排序。ORDER BY 支持两种排序方式:

  • ASC :升序(默认)
  • DESC :降序

单列排序示例:

1
2
3
SELECT prod_name
FROM Products
ORDER BY prod_name;

可以按多个列进行排序,并且为每个列指定不同的排序方式。

多列排序示例:

1
2
SELECT * FROM Products
ORDER BY prod_price DESC, prod_name ASC;

按列位置排序(不推荐):

1
2
3
SELECT prod_id, prod_price, prod_name
FROM Products
ORDER BY 2, 3;

GROUP BY

GROUP BY 子句将记录分组到汇总行中,GROUP BY 为每个组返回一个记录。

GROUP BY 要点:

  • GROUP BY 子句可以包含任意数目的列,因而可以对分组进行嵌套,更细致地进行数据分组。
  • 如果在 GROUP BY 子句中嵌套了分组,数据将在最后指定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。
  • GROUP BY 子句中列出的每一列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在 SELECT 中使用表达式,则必须在 GROUP BY 子句中指定相同的表达式。不能使用别名。
  • 大多数 SQL 实现不允许 GROUP BY 列带有长度可变的数据类型(如文本或备注型字段)。
  • 除聚集计算语句外,SELECT 语句中的每一列都必须在 GROUP BY 子句中给出。
  • 如果分组列中包含具有 NULL 值的行,则 NULL 将作为一个分组返回。如果列中有多行 NULL 值,它们将分为一组。
  • GROUP BY 子句必须出现在 WHERE 子句之后,ORDER BY 子句之前。

分组示例:

1
2
SELECT cust_name, COUNT(cust_address) AS addr_num
FROM Customers GROUP BY cust_name;

分组后排序示例:

1
2
3
SELECT cust_name, COUNT(cust_address) AS addr_num
FROM Customers GROUP BY cust_name
ORDER BY cust_name DESC;

HAVING

HAVING 用于对汇总的 GROUP BY 结果进行过滤。HAVING 要求存在一个 GROUP BY 子句。

WHEREHAVING 可以在相同的查询中。

HAVING vs WHERE

  • HAVING 非常类似于 WHEREWHEREHAVING 都是用于过滤。
  • WHERE 过滤行,而 HAVING 过滤分组。

使用 WHEREHAVING 过滤数据示例:

过滤两个以上订单的分组

1
2
3
4
SELECT cust_id, COUNT(*) AS orders
FROM Orders
GROUP BY cust_id
HAVING COUNT(*) >= 2;

列出具有两个以上产品且其价格大于等于 4 的供应商:

1
2
3
4
5
SELECT vend_id, COUNT(*) AS num_prods
FROM Products
WHERE prod_price >= 4
GROUP BY vend_id
HAVING COUNT(*) >= 2;

检索包含三个或更多物品的订单号和订购物品的数目:

1
2
3
4
SELECT order_num, COUNT(*) AS items
FROM orderitems
GROUP BY order_num
HAVING COUNT(*) >= 3;

要按订购物品的数目排序输出,需要添加 ORDER BY 子句

1
2
3
4
5
SELECT order_num, COUNT(*) AS items
FROM orderitems
GROUP BY order_num
HAVING COUNT(*) >= 3
ORDER BY items, order_num;

函数

🔔 注意:不同数据库的函数往往各不相同,因此不可移植。本节主要以 Mysql 的函数为例。

字符串函数

函数 说明
CONCAT() 合并字符串
LEFT()RIGHT() 左边或者右边的字符
LOWER()UPPER() 转换为小写或者大写
LTRIM()RTIM() 去除左边或者右边的空格
LENGTH() 长度
SOUNDEX() 转换为语音值

其中, SOUNDEX() 可以将一个字符串转换为描述其语音表示的字母数字模式。

以下为部分字符串函数的使用示例

拼接字符串值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Access 和 SQL Server
SELECT vend_name + ' (' + vend_country + ')'
FROM Vendors
ORDER BY vend_name;

-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base
SELECT vend_name || ' (' || vend_country || ')'
FROM Vendors
ORDER BY vend_name;

-- MySQL 或 MariaDB
SELECT Concat(vend_name, ' (', vend_country, ')')
FROM Vendors
ORDER BY vend_name;

去除字符串中的空格:

1
2
3
4
5
6
7
8
9
-- Access 和 SQL Server
SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')'
FROM Vendors
ORDER BY vend_name;

-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base
SELECT RTRIM(vend_name) || ' (' || RTRIM(vend_country) || ')'
FROM Vendors
ORDER BY vend_name;

时间函数

  • 日期格式:YYYY-MM-DD
  • 时间格式:HH:MM:SS
函 数 说 明
ADDDATE() 增加一个日期(天、周等)
ADDTIME() 增加一个时间(时、分等)
CURRENT_DATE() 返回当前日期
CURRENT_TIME() 返回当前时间
DATE() 返回日期时间的日期部分
DATEDIFF() 计算两个日期之差
DATE_ADD() 高度灵活的日期运算函数
DATE_FORMAT() 返回一个格式化的日期或时间串
DAY() 返回一个日期的天数部分
DAYOFWEEK() 对于一个日期,返回对应的星期几
HOUR() 返回一个时间的小时部分
MINUTE() 返回一个时间的分钟部分
MONTH() 返回一个日期的月份部分
NOW() 返回当前日期和时间
SECOND() 返回一个时间的秒部分
TIME() 返回一个日期时间的时间部分
YEAR() 返回一个日期的年份部分

部分日期和时间处理函数使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- SQL Server
SELECT order_num
FROM Orders
WHERE DATEPART(yy, order_date) = 2012;

-- Access
SELECT order_num
FROM Orders
WHERE DATEPART('yyyy', order_date) = 2012;

-- PostgreSQL
SELECT order_num
FROM Orders
WHERE DATE_PART('year', order_date) = 2012;

-- Oracle
SELECT order_num
FROM Orders
WHERE to_number(to_char(order_date, 'YYYY')) = 2012;

-- MySQL 和 MariaDB
SELECT order_num
FROM Orders
WHERE YEAR(order_date) = 2012;

数学函数

常见 Mysql 数学函数:

函数 说明
ABS() 返回一个数的绝对值
COS() 返回一个角度的余弦
EXP() 返回一个数的指数值
PI() 返回圆周率
SIN() 返回一个角度的正弦
SQRT() 返回一个数的平方根
TAN() 返回一个角度的正切

聚合函数

函 数 说 明
AVG() 返回某列的平均值
COUNT() 返回某列的行数
MAX() 返回某列的最大值
MIN() 返回某列的最小值
SUM() 返回某列值之和

AVG() 通过对表中行数计数并计算其列值之和,求得该列的平均值。

使用 DISTINCT 可以让汇总函数值汇总不同的值。

::: tabs#聚合函数示例

@tab AVG() 示例

使用 AVG() 返回 Products 表中所有产品的平均价格:

1
2
SELECT AVG(prod_price) AS avg_price
FROM Products;

@tab COUNT() 示例

COUNT() 函数进行计数。可利用 COUNT() 确定表中行的数目或符合特定条件的行的数目。

返回 Customers 表中顾客的总数:

1
2
SELECT COUNT(*) AS num_cust
FROM Customers;

只对具有电子邮件地址的客户计数:

1
2
SELECT COUNT(cust_email) AS num_cust
FROM Customers;

@tab MAX() 示例

返回 Products 表中最贵物品的价格:

1
2
SELECT MAX(prod_price) AS max_price
FROM Products;

@tab MIN() 示例

返回 Products 表中最便宜物品的价格

1
2
SELECT MIN(prod_price) AS min_price
FROM Products;

@tab SUM() 示例

返回订单中所有物品数量之和

1
2
3
SELECT SUM(quantity) AS items_ordered
FROM OrderItems
WHERE order_num = 20005;

:::

转换函数

函 数 说 明 示例
CAST() 转换数据类型 SELECT CAST("2017-08-29" AS DATE); -> 2017-08-29

事务

不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATEDROP 语句。

MySQL 默认采用隐式提交策略(autocommit,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMITROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。

通过 set autocommit=0 可以取消自动提交,直到 set autocommit=1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。

事务处理指令:

  • START TRANSACTION - 指令用于标记事务的起始点。
  • SAVEPOINT - 指令用于创建保留点。
  • ROLLBACK TO - 指令用于回滚到指定的保留点;如果没有设置保留点,则回退到 START TRANSACTION 语句处。
  • COMMIT - 提交事务。
  • RELEASE SAVEPOINT:删除某个保存点。
  • SET TRANSACTION:设置事务的隔离级别。

事务处理示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 开始事务
START TRANSACTION;

-- 插入操作 A
INSERT INTO `user`
VALUES (1, 'root1', 'root1', 'xxxx@163.com');

-- 创建保留点 updateA
SAVEPOINT updateA;

-- 插入操作 B
INSERT INTO `user`
VALUES (2, 'root2', 'root2', 'xxxx@163.com');

-- 回滚到保留点 updateA
ROLLBACK TO updateA;

-- 提交事务,只有操作 A 生效
COMMIT;

(以下为 DCL 语句用法)

权限控制

GRANTREVOKE 可在几个层次上控制访问权限:

  • 整个服务器,使用 GRANT ALLREVOKE ALL
  • 整个数据库,使用 ON database.*;
  • 特定的表,使用 ON database.table;
  • 特定的列;
  • 特定的存储过程。

新创建的账户没有任何权限。

账户用 username@host 的形式定义,username@% 使用的是默认主机名。

MySQL 的账户信息保存在 mysql 这个数据库中。

1
2
USE mysql;
SELECT user FROM user;

创建账户

1
CREATE USER myuser IDENTIFIED BY 'mypassword';

修改账户名

1
2
UPDATE user SET user='newuser' WHERE user='myuser';
FLUSH PRIVILEGES;

删除账户

1
DROP USER myuser;

查看权限

1
SHOW GRANTS FOR myuser;

授予权限

1
GRANT SELECT, INSERT ON *.* TO myuser;

删除权限

1
REVOKE SELECT, INSERT ON *.* FROM myuser;

更改密码

1
SET PASSWORD FOR myuser = 'mypass';

存储过程

存储过程的英文是 Stored Procedure。它可以视为一组 SQL 语句的批处理。一旦存储过程被创建出来,使用它就像使用函数一样简单,我们直接通过调用存储过程名即可。

定义存储过程的语法格式:

1
2
3
4
CREATE PROCEDURE 存储过程名称 ([参数列表])
BEGIN
需要执行的语句
END

存储过程定义语句类型:

  • CREATE PROCEDURE 用于创建存储过程
  • DROP PROCEDURE 用于删除存储过程
  • ALTER PROCEDURE 用于修改存储过程

使用存储过程

创建存储过程的要点:

  • DELIMITER 用于定义语句的结束符
  • 存储过程的 3 种参数类型:
    • IN:存储过程的入参
    • OUT:存储过程的出参
    • INPUT:既是存储过程的入参,也是存储过程的出参
  • 流控制语句:
    • BEGIN…ENDBEGIN…END 中间包含了多个语句,每个语句都以(;)号为结束符。
    • DECLAREDECLARE 用来声明变量,使用的位置在于 BEGIN…END 语句中间,而且需要在其他语句使用之前进行变量的声明。
    • SET:赋值语句,用于对变量进行赋值。
    • SELECT…INTO:把从数据表中查询的结果存放到变量中,也就是为变量赋值。每次只能给一个变量赋值,不支持集合的操作。
    • IF…THEN…ENDIF:条件判断语句,可以在 IF…THEN…ENDIF 中使用 ELSEELSEIF 来进行条件判断。
    • CASECASE 语句用于多条件的分支判断。

创建存储过程示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
DROP PROCEDURE IF EXISTS `proc_adder`;
DELIMITER ;;
CREATE DEFINER=`root`@`localhost` PROCEDURE `proc_adder`(IN a int, IN b int, OUT sum int)
BEGIN
DECLARE c int;
if a is null then set a = 0;
end if;

if b is null then set b = 0;
end if;

set sum = a + b;
END
;;
DELIMITER ;

使用存储过程示例:

1
2
3
set @b=5;
call proc_adder(2,@b,@s);
select @s as sum;

存储过程的利弊

存储过程的优点:

  • 执行效率高:一次编译多次使用。
  • 安全性强:在设定存储过程的时候可以设置对用户的使用权限,这样就和视图一样具有较强的安全性。
  • 可复用:将代码封装,可以提高代码复用。
  • 性能好
    • 由于是预先编译,因此具有很高的性能。
    • 一个存储过程替代大量 T_SQL 语句 ,可以降低网络通信量,提高通信速率。

存储过程的缺点:

  • 可移植性差:存储过程不能跨数据库移植。由于不同数据库的存储过程语法几乎都不一样,十分难以维护(不通用)。
  • 调试困难:只有少数 DBMS 支持存储过程的调试。对于复杂的存储过程来说,开发和维护都不容易。
  • 版本管理困难:比如数据表索引发生变化了,可能会导致存储过程失效。我们在开发软件的时候往往需要进行版本管理,但是存储过程本身没有版本控制,版本迭代更新的时候很麻烦。
  • 不适合高并发的场景:高并发的场景需要减少数据库的压力,有时数据库会采用分库分表的方式,而且对可扩展性要求很高,在这种情况下,存储过程会变得难以维护,增加数据库的压力,显然就不适用了。

_综上,存储过程的优缺点都非常突出,是否使用一定要慎重,需要根据具体应用场景来权衡_。

触发器

触发器是特殊的存储过程,它在特定的数据库活动发生时自动执行。触发器可以与特定表上的 INSERT、UPDATE 和 DELETE 操作(或组合)相关联。

触发器是一种与表操作有关的数据库对象,当触发器所在表上出现指定事件时,将调用该对象,即表的操作事件触发表上的触发器的执行。

触发器的一些常见用途

  • 保证数据一致。例如,在 INSERT 或 UPDATE 操作中将所有州名转换为大写。
  • 基于某个表的变动在其他表上执行活动。例如,每当更新或删除一行时将审计跟踪记录写入某个日志表。
  • 进行额外的验证并根据需要回退数据。例如,保证某个顾客的可用资金不超限定,如果已经超出,则阻塞插入。
  • 计算计算列的值或更新时间戳。

触发器特性

可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。

MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储过程。

BEGINEND

当触发器的触发条件满足时,将会执行 BEGINEND 之间的触发器执行动作。

🔔 注意:在 MySQL 中,分号 ; 是语句结束的标识符,遇到分号表示该段语句已经结束,MySQL 可以开始执行了。因此,解释器遇到触发器执行动作中的分号后就开始执行,然后会报错,因为没有找到和 BEGIN 匹配的 END。

这时就会用到 DELIMITER 命令(DELIMITER 是定界符,分隔符的意思)。它是一条命令,不需要语句结束标识,语法为:DELIMITER new_delemiternew_delemiter 可以设为 1 个或多个长度的符号,默认的是分号 ;,我们可以把它修改为其他符号,如 $ - DELIMITER $ 。在这之后的语句,以分号结束,解释器不会有什么反应,只有遇到了 $,才认为是语句结束。注意,使用完之后,我们还应该记得把它给修改回来。

NEWOLD

  • MySQL 中定义了 NEWOLD 关键字,用来表示触发器的所在表中,触发了触发器的那一行数据。
  • INSERT 型触发器中,NEW 用来表示将要(BEFORE)或已经(AFTER)插入的新数据;
  • UPDATE 型触发器中,OLD 用来表示将要或已经被修改的原数据,NEW 用来表示将要或已经修改为的新数据;
  • DELETE 型触发器中,OLD 用来表示将要或已经被删除的原数据;
  • 使用方法: NEW.columnName (columnName 为相应数据表某一列名)

触发器指令

提示:为了理解触发器的要点,有必要先了解一下创建触发器的指令。

CREATE TRIGGER 指令用于创建触发器。

语法:

1
2
3
4
5
6
7
8
CREATE TRIGGER trigger_name
trigger_time
trigger_event
ON table_name
FOR EACH ROW
BEGIN
trigger_statements
END;

说明:

  • trigger_name:触发器名
  • trigger_time: 触发器的触发时机。取值为 BEFOREAFTER
  • trigger_event: 触发器的监听事件。取值为 INSERTUPDATEDELETE
  • table_name: 触发器的监听目标。指定在哪张表上建立触发器。
  • FOR EACH ROW: 行级监视,Mysql 固定写法,其他 DBMS 不同。
  • trigger_statements: 触发器执行动作。是一条或多条 SQL 语句的列表,列表内的每条语句都必须用分号 ; 来结尾。

创建触发器示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- SQL Server
CREATE TRIGGER customer_state
ON Customers
FOR INSERT, UPDATE
AS
UPDATE Customers
SET cust_state = Upper(cust_state)
WHERE Customers.cust_id = inserted.cust_id;

-- Oracle 和 PostgreSQL
CREATE TRIGGER customer_state
AFTER INSERT OR UPDATE
FOR EACH ROW
BEGIN
UPDATE Customers
SET cust_state = Upper(cust_state)
WHERE Customers.cust_id = :OLD.cust_id
END;

查看触发器示例:

1
SHOW TRIGGERS;

删除触发器示例:

1
DROP TRIGGER IF EXISTS trigger_insert_user;

游标

游标(CURSOR)是一个存储在 DBMS 服务器上的数据库查询,它不是一条 SELECT 语句,而是被该语句检索出来的结果集。在存储过程中使用游标可以对一个结果集进行移动遍历。

游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。

游标要点

  • 能够标记游标为只读,使数据能读取,但不能更新和删除。
  • 能控制可以执行的定向操作(向前、向后、第一、最后、绝对位置、相对位置等)。
  • 能标记某些列为可编辑的,某些列为不可编辑的。
  • 规定范围,使游标对创建它的特定请求(如存储过程)或对所有请求可访问。
  • 指示 DBMS 对检索出的数据(而不是指出表中活动数据)进行复制,使数据在游标打开和访问期间不变化。

使用游标的步骤:

  1. 定义游标:通过 DECLARE cursor_name CURSOR FOR <语句> 定义游标。这个过程没有实际检索出数据。
  2. 打开游标:通过 OPEN cursor_name 打开游标。
  3. 取出数据:通过 FETCH cursor_name INTO var_name ... 获取数据。
  4. 关闭游标:通过 CLOSE cursor_name 关闭游标。
  5. 释放游标:通过 DEALLOCATE PREPARE 释放游标。

游标使用示例:

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
DELIMITER $
CREATE PROCEDURE getTotal()
BEGIN
DECLARE total INT;
-- 创建接收游标数据的变量
DECLARE sid INT;
DECLARE sname VARCHAR(10);
-- 创建总数变量
DECLARE sage INT;
-- 创建结束标志变量
DECLARE done INT DEFAULT false;
-- 创建游标
DECLARE cur CURSOR FOR SELECT id,name,age from cursor_table where age>30;
-- 指定游标循环结束时的返回值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true;
SET total = 0;
-- 打开游标
OPEN cur;
FETCH cur INTO sid, sname, sage;
WHILE(NOT done)
DO
SET total = total + 1;
FETCH cur INTO sid, sname, sage;
END WHILE;
-- 关闭游标
CLOSE cur;
SELECT total;
END $
DELIMITER ;

-- 调用存储过程
call getTotal();

参考资料

红黑树

平衡二叉树

平衡二叉树的严格定义是这样的:二叉树中任意一个节点的左右子树的高度相差不能大于 1。

完全二叉树、满二叉树其实都是平衡二叉树,但是非完全二叉树也有可能是平衡二叉树。

img

平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比较“平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些

什么是红黑树

红黑树的英文是“Red-Black Tree”,简称 R-B Tree。它是一种不严格的平衡二叉查找树。

红黑树中的节点,一类被标记为黑色,一类被标记为红色。除此之外,一棵红黑树还需要满足这样几个要求:

  • 根节点是黑色的;
  • 每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据;
  • 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
  • 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;

img

为什么说红黑树是“近似平衡”的?

平衡二叉查找树的初衷,是为了解决二叉查找树因为动态更新导致的性能退化问题。

所以,“平衡”的意思可以等价为性能不退化。“近似平衡”就等价为性能不会退化的太严重

如果我们将红色节点从红黑树中去掉,那单纯包含黑色节点的红黑树的高度是多少呢

红色节点删除之后,有些节点就没有父节点了,它们会直接拿这些节点的祖父节点(父节点的父节点)作为父节点。所以,之前的二叉树就变成了四叉树。

img

前面红黑树的定义里有这么一条:从任意节点到可达的叶子节点的每个路径包含相同数目的黑色节点。我们从四叉树中取出某些节点,放到叶节点位置,四叉树就变成了完全二叉树。所以,仅包含黑色节点的四叉树的高度,比包含相同节点个数的完全二叉树的高度还要小。

现在把红色节点加回去,高度会变成多少呢

在红黑树中,红色节点不能相邻,也就是说,有一个红色节点就要至少有一个黑色节点,将它跟其他红色节点隔开。红黑树中包含最多黑色节点的路径不会超过 log2n,所以加入红色节点之后,最长路径不会超过 2log2n,也就是说,红黑树的高度近似 2log2n。

所以,红黑树的高度只比高度平衡的 AVL 树的高度(log2n)仅仅大了一倍,在性能上,下降得并不多。这样推导出来的结果不够精确,实际上红黑树的性能更好。

为什么需要红黑树

AVL 树是一种高度平衡的二叉树,所以查找的效率非常高,但是,有利就有弊,AVL 树为了维持这种高度的平衡,就要付出更多的代价。每次插入、删除都要做调整,就比较复杂、耗时。所以,对于有频繁的插入、删除操作的数据集合,使用 AVL 树的代价就有点高了。

红黑树只是做到了近似平衡,并不是严格的平衡,所以在维护平衡的成本上,要比 AVL 树要低。

所以,红黑树的插入、删除、查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支撑这种工业级的应用,我们更倾向于这种性能稳定的平衡二叉查找树。

红黑树平衡调整

插入操作的平衡调整

红黑树规定,插入的节点必须是红色的。而且,二叉查找树中新插入的节点都是放在叶子节点上

  • 如果插入节点的父节点是黑色的,那我们什么都不用做,它仍然满足红黑树的定义。
  • 如果插入的节点是根节点,那我们直接改变它的颜色,把它变成黑色就可以了。

除此之外,其他情况都会违背红黑树的定义,于是我们就需要进行调整,调整的过程包含两种基础的操作:左右旋转改变颜色

红黑树的平衡调整过程是一个迭代的过程。我们把正在处理的节点叫作关注节点。关注节点会随着不停地迭代处理,而不断发生变化。最开始的关注节点就是新插入的节点。

新节点插入之后,如果红黑树的平衡被打破,那一般会有下面三种情况。我们只需要根据每种情况的特点,不停地调整,就可以让红黑树继续符合定义,也就是继续保持平衡。

CASE 1:如果关注节点是 a,它的叔叔节点 d 是红色,我们就依次执行下面的操作:

  • 将关注节点 a 的父节点 b、叔叔节点 d 的颜色都设置成黑色;
  • 将关注节点 a 的祖父节点 c 的颜色设置成红色;
  • 关注节点变成 a 的祖父节点 c;
  • 跳到 CASE 2 或者 CASE 3。

img

CASE 2:如果关注节点是 a,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b 的右子节点,我们就依次执行下面的操作:

  • 关注节点变成节点 a 的父节点 b;
  • 围绕新的关注节点 b 左旋;
  • 跳到 CASE 3。

img

CASE 3:如果关注节点是 a,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b 的左子节点,我们就依次执行下面的操作:

  • 围绕关注节点 a 的祖父节点 c 右旋;
  • 将关注节点 a 的父节点 b、兄弟节点 c 的颜色互换。
  • 调整结束。

img

删除操作的平衡调整

针对删除节点初步调整

CASE 1:如果要删除的节点是 a,它只有一个子节点 b,那我们就依次进行下面的操作:

  • 删除节点 a,并且把节点 b 替换到节点 a 的位置,这一部分操作跟普通的二叉查找树的删除操作一样;
  • 节点 a 只能是黑色,节点 b 也只能是红色,其他情况均不符合红黑树的定义。这种情况下,我们把节点 b 改为黑色;
  • 调整结束,不需要进行二次调整。

img

CASE 2:如果要删除的节点 a 有两个非空子节点,并且它的后继节点就是节点 a 的右子节点 c。我们就依次进行下面的操作:

  • 如果节点 a 的后继节点就是右子节点 c,那右子节点 c 肯定没有左子树。我们把节点 a 删除,并且将节点 c 替换到节点 a 的位置。这一部分操作跟普通的二叉查找树的删除操作无异;
  • 然后把节点 c 的颜色设置为跟节点 a 相同的颜色;
  • 如果节点 c 是黑色,为了不违反红黑树的最后一条定义,我们给节点 c 的右子节点 d 多加一个黑色,这个时候节点 d 就成了“红 - 黑”或者“黑 - 黑”;
  • 这个时候,关注节点变成了节点 d,第二步的调整操作就会针对关注节点来做。

CASE 3:如果要删除的是节点 a,它有两个非空子节点,并且节点 a 的后继节点不是右子节点,我们就依次进行下面的操作:

  • 找到后继节点 d,并将它删除,删除后继节点 d 的过程参照 CASE 1;
  • 将节点 a 替换成后继节点 d;
  • 把节点 d 的颜色设置为跟节点 a 相同的颜色;
  • 如果节点 d 是黑色,为了不违反红黑树的最后一条定义,我们给节点 d 的右子节点 c 多加一个黑色,这个时候节点 c 就成了“红 - 黑”或者“黑 - 黑”;
  • 这个时候,关注节点变成了节点 c,第二步的调整操作就会针对关注节点来做。

针对关注节点进行二次调整

CASE 1:如果关注节点是 a,它的兄弟节点 c 是红色的,我们就依次进行下面的操作:

  • 围绕关注节点 a 的父节点 b 左旋;
  • 关注节点 a 的父节点 b 和祖父节点 c 交换颜色;
  • 关注节点不变;
  • 继续从四种情况中选择适合的规则来调整。

CASE 2:如果关注节点是 a,它的兄弟节点 c 是黑色的,并且节点 c 的左右子节点 d、e 都是黑色的,我们就依次进行下面的操作:

  • 将关注节点 a 的兄弟节点 c 的颜色变成红色;
  • 从关注节点 a 中去掉一个黑色,这个时候节点 a 就是单纯的红色或者黑色;
  • 给关注节点 a 的父节点 b 添加一个黑色,这个时候节点 b 就变成了“红 - 黑”或者“黑 - 黑”;
  • 关注节点从 a 变成其父节点 b;
  • 继续从四种情况中选择符合的规则来调整。

CASE 3:如果关注节点是 a,它的兄弟节点 c 是黑色,c 的左子节点 d 是红色,c 的右子节点 e 是黑色,我们就依次进行下面的操作:

  • 围绕关注节点 a 的兄弟节点 c 右旋;
  • 节点 c 和节点 d 交换颜色;
  • 关注节点不变;
  • 跳转到 CASE 4,继续调整。

CASE 4:如果关注节点 a 的兄弟节点 c 是黑色的,并且 c 的右子节点是红色的,我们就依次进行下面的操作:

  • 围绕关注节点 a 的父节点 b 左旋;
  • 将关注节点 a 的兄弟节点 c 的颜色,跟关注节点 a 的父节点 b 设置成相同的颜色;
  • 将关注节点 a 的父节点 b 的颜色设置为黑色;
  • 从关注节点 a 中去掉一个黑色,节点 a 就变成了单纯的红色或者黑色;
  • 将关注节点 a 的叔叔节点 e 设置为黑色;
  • 调整结束。

参考资料

Samba 应用

samba 是在 Linux 和 UNIX 系统上实现 SMB 协议的一个免费软件。

samba 提供了在不同计算机(即使操作系统不同)上共享服务的能力。

关键词:samba, selinux

安装配置 samba

本文将以一个完整的示例来展示如何配置 samba 来实现 Linux 和 Windows 的文件共享。

目标:假设希望共享 Linux 服务器上的 /share/fs 目录。

查看是否已经安装 samba

  • CentOS:rpm -qa | grep samba
  • Ubuntu:dpkg -l | grep samba

安装 samba 工具

  • CentOS:yum install -y samba samba-client samba-common
  • Ubuntu:sudo apt-get install -y samba samba-client

配置 samba

samba 服务的配置文件是 /etc/samba/smb.conf,如果没有则 samba 无法启动。

执行以下命令,编辑配置文件:

1
vim /etc/samba/smb.conf

修改配置如下:

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
[global]
workgroup = SAMBA
security = user

passdb backend = tdbsam

printing = cups
printcap name = cups
load printers = yes
cups options = raw

[homes]
comment = Home Directories
valid users = %S, %D%w%S
browseable = No
read only = No
inherit acls = Yes

[printers]
comment = All Printers
path = /var/tmp
printable = Yes
create mask = 0600
browseable = No

[print$]
comment = Printer Drivers
path = /var/lib/samba/drivers
write list = @printadmin root
force group = @printadmin
create mask = 0664
directory mask = 0775

[fs]
comment = share folder
path = /share/fs
browseable = yes
writable = yes
read only = no
guest ok = yes
create mask = 0777
directory mask = 0777
public = yes
valid users = root

说明:

  • 我在这里添加了一个 [fs] 标签,这就是共享区域的配置。
  • 这里设置 path 属性为 /share/fs,意味着准备共享 /share/fs 目录,需要根据实际需要设置路径。/share/fs 目录的权限要设置为 777chmod 777 /share/fs
  • browseablewritable 等属性就比较容易理解了,即配置共享目录的访问权限。
  • valid users 属性指定允许访问的用户,需要注意的是指定的用户必须是 Linux 机器上实际存在的用户。

创建 samba 用户

创建的 samba 用户必须是 Linux 机器上实际存在的用户。

1
2
3
4
$ sudo smbpasswd -a root
New SMB password:
Retype new SMB password:
Added user root.

根据提示输入 samba 用户的密码。当 samba 服务成功安装、启动后,通过 Windows 系统访问机器共享目录时,就要输入这里配置的用户名、密码。

  • 查看 samba 服务器中已拥有哪些用户 - pdbedit -L
  • 删除 samba 服务中的某个用户 - smbpasswd -x 用户名

启动 samba 服务

CentOS 6

1
2
$ sudo service samba restart  # 重启 samba
$ sudo service smb restart # 重启 samba

CentOS 7

1
2
3
4
$ sudo systemctl start smb.service     # 启动 samba
$ sudo systemctl restart smb.service # 重启 samba
$ sudo systemctl enable smb.service # 设置开机自动启动
$ sudo systemctl status smb.service # 查询 samba 状态

Ubuntu 16.04.3

1
$ sudo service smbd restart

为 samba 添加防火墙规则

1
2
$ sudo firewall-cmd --permanent --zone=public --add-service=samba
$ sudo firewall-cmd --reload

测试 samba 服务

1
$ smbclient //localhost/fs -U root

输入 samba 用户的密码,如果成功,就会进入 smb: \>

访问 samba 服务共享的目录

Windows:

访问:\\<你的ip>\<你的共享路径>

img

Mac:

与 Windows 类似,直接在 Finder 中访问 smb://<你的ip>/<你的共享路径> 即可。

配置详解

samba 默认配置

你可以从 这里 获取到默认配置文件:

1
2
$ cp /etc/samba/smb.conf /etc/samba/smb.conf.bak
$ wget "https://git.samba.org/samba.git/?p=samba.git;a=blob_plain;f=examples/smb.conf.default;hb=HEAD" -O /etc/samba/smb.conf

smb.conf 默认内容如下:

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
[global]
workgroup = SAMBA
security = user

passdb backend = tdbsam

printing = cups
printcap name = cups
load printers = yes
cups options = raw

[homes]
comment = Home Directories
valid users = %S, %D%w%S
browseable = No
read only = No
inherit acls = Yes

[printers]
comment = All Printers
path = /var/tmp
printable = Yes
create mask = 0600
browseable = No

[print$]
comment = Printer Drivers
path = /var/lib/samba/drivers
write list = root
create mask = 0664
directory mask = 0775

全局参数 [global]

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
[global]

config file = /usr/local/samba/lib/smb.conf.%m
说明:config file可以让你使用另一个配置文件来覆盖缺省的配置文件。如果文件 不存在,则该项无效。这个参数很有用,可以使得samba配置更灵活,可以让一台samba服务器模拟多台不同配置的服务器。比如,你想让PC1(主机名)这台电脑在访问Samba Server时使用它自己的配置文件,那么先在/etc/samba/host/下为PC1配置一个名为smb.conf.pc1的文件,然后在smb.conf中加入:config file=/etc/samba/host/smb.conf.%m。这样当PC1请求连接Samba Server时,smb.conf.%m就被替换成smb.conf.pc1。这样,对于PC1来说,它所使用的Samba服务就是由smb.conf.pc1定义的,而其他机器访问Samba Server则还是应用smb.conf。

workgroup = WORKGROUP
说明:设定 Samba Server 所要加入的工作组或者域。

server string = Samba Server Version %v
说明:设定 Samba Server 的注释,可以是任何字符串,也可以不填。宏%v表示显示Samba的版本号。

netbios name = smbserver
说明:设置Samba Server的NetBIOS名称。如果不填,则默认会使用该服务器的DNS名称的第一部分。netbios name和workgroup名字不要设置成一样了。

interfaces = lo eth0 192.168.12.2/24 192.168.13.2/24
说明:设置Samba Server监听哪些网卡,可以写网卡名,也可以写该网卡的IP地址。

hosts allow = 127.192.168.1 192.168.10.1
说明:表示允许连接到Samba Server的客户端,多个参数以空格隔开。可以用一个IP表示,也可以用一个网段表示。hosts deny 与hosts allow 刚好相反。
例如:
# 表示容许来自172.17.2.*.*的主机连接,但排除172.17.2.50
hosts allow=172.17.2.EXCEPT172.17.2.50
# 表示容许来自172.17.2.0/255.255.0.0子网中的所有主机连接
hosts allow=172.17.2.0/255.255.0.0
# 表示容许来自M1和M2两台计算机连接
hosts allow=M1,M2
# 表示容许来自SC域的所有计算机连接
hosts allow=@SC
max connections = 0
说明:max connections用来指定连接Samba Server的最大连接数目。如果超出连接数目,则新的连接请求将被拒绝。0表示不限制。

deadtime = 0
说明:deadtime用来设置断掉一个没有打开任何文件的连接的时间。单位是分钟,0代表Samba Server不自动切断任何连接。

time server = yes/no
说明:time server用来设置让nmdb成为windows客户端的时间服务器。

log file = /var/log/samba/log.%m
说明:设置Samba Server日志文件的存储位置以及日志文件名称。在文件名后加个宏%m(主机名),表示对每台访问Samba Server的机器都单独记录一个日志文件。如果pc1、pc2访问过Samba Server,就会在/var/log/samba目录下留下log.pc1和log.pc2两个日志文件。

max log size = 50
说明:设置Samba Server日志文件的最大容量,单位为kB,0代表不限制。

security = user
说明:设置用户访问Samba Server的验证方式,一共有四种验证方式。
1. share:用户访问Samba Server不需要提供用户名和口令, 安全性能较低。
2. user:Samba Server共享目录只能被授权的用户访问,由Samba Server负责检查账号和密码的正确性。账号和密码要在本Samba Server中建立。
3. server:依靠其他Windows NT/2000或Samba Server来验证用户的账号和密码,是一种代理验证。此种安全模式下,系统管理员可以把所有的Windows用户和口令集中到一个NT系统上,使用Windows NT进行Samba认证, 远程服务器可以自动认证全部用户和口令,如果认证失败,Samba将使用用户级安全模式作为替代的方式。
4. domain:域安全级别,使用主域控制器(PDC)来完成认证。

passdb backend = tdbsam
说明:passdb backend就是用户后台的意思。目前有三种后台:smbpasswd、tdbsam和ldapsam。sam应该是security account manager(安全账户管理)的简写。

smbpasswd:该方式是使用smb自己的工具smbpasswd来给系统用户(真实
用户或者虚拟用户)设置一个Samba密码,客户端就用这个密码来访问Samba的资源。
1. smbpasswd文件默认在/etc/samba目录下,不过有时候要手工建立该文件。
2. tdbsam:该方式则是使用一个数据库文件来建立用户数据库。数据库文件叫passdb.tdb,默认在/etc/samba目录下。passdb.tdb用户数据库可以使用smbpasswd –a来建立Samba用户,不过要建立的Samba用户必须先是系统用户。我们也可以使用pdbedit命令来建立Samba账户。pdbedit命令的参数很多,我们列出几个主要的。
pdbedit –a username:新建Samba账户。
pdbedit –x username:删除Samba账户。
pdbedit –L:列出Samba用户列表,读取passdb.tdb数据库文件。
pdbedit –Lv:列出Samba用户列表的详细信息。
pdbedit –c “[D]” –u username:暂停该Samba用户的账号。
pdbedit –c “[]” –u username:恢复该Samba用户的账号。
3. ldapsam:该方式则是基于LDAP的账户管理方式来验证用户。首先要建立LDAP服务,然后设置“passdb backend = ldapsam:ldap://LDAP Server”

encrypt passwords = yes/no
说明:是否将认证密码加密。因为现在windows操作系统都是使用加密密码,所以一般要开启此项。不过配置文件默认已开启。

smb passwd file = /etc/samba/smbpasswd
说明:用来定义samba用户的密码文件。smbpasswd文件如果没有那就要手工新建。

username map = /etc/samba/smbusers
说明:用来定义用户名映射,比如可以将root换成administrator、admin等。不过要事先在smbusers文件中定义好。比如:root = administrator admin,这样就可以用administrator或admin这两个用户来代替root登陆Samba Server,更贴近windows用户的习惯。

guest account = nobody
说明:用来设置guest用户名。

socket options = TCP_NODELAY SO_RCVBUF=8192 SO_SNDBUF=8192
说明:用来设置服务器和客户端之间会话的Socket选项,可以优化传输速度。

domain master = yes/no
说明:设置Samba服务器是否要成为网域主浏览器,网域主浏览器可以管理跨子网域的浏览服务。

local master = yes/no
说明:local master用来指定Samba Server是否试图成为本地网域主浏览器。如果设为no,则永远不会成为本地网域主浏览器。但是即使设置为yes,也不等于该Samba Server就能成为主浏览器,还需要参加选举。

preferred master = yes/no
说明:设置Samba Server一开机就强迫进行主浏览器选举,可以提高Samba Server成为本地网域主浏览器的机会。如果该参数指定为yes时,最好把domain master也指定为yes。使用该参数时要注意:如果在本Samba Server所在的子网有其他的机器(不论是windows NT还是其他Samba Server)也指定为首要主浏览器时,那么这些机器将会因为争夺主浏览器而在网络上大发广播,影响网络性能。如果同一个区域内有多台Samba Server,将上面三个参数设定在一台即可。

os level = 200
说明:设置samba服务器的os level。该参数决定Samba Server是否有机会成为本地网域的主浏览器。os level从0到255,winNT的os level是32,win95/98的os level是1。Windows 2000的os level是64。如果设置为0,则意味着Samba Server将失去浏览选择。如果想让Samba Server成为PDC,那么将它的os level值设大些。

domain logons = yes/no
说明:设置Samba Server是否要做为本地域控制器。主域控制器和备份域控制器都需要开启此项。

logon . = %u.bat
说明:当使用者用windows客户端登陆,那么Samba将提供一个登陆档。如果设置成%u.bat,那么就要为每个用户提供一个登陆档。如果人比较多,那就比较麻烦。可以设置成一个具体的文件名,比如start.bat,那么用户登陆后都会去执行start.bat,而不用为每个用户设定一个登陆档了。这个文件要放置在[netlogon]的path设置的目录路径下。

wins support = yes/no
说明:设置samba服务器是否提供wins服务。

wins server = wins服务器IP地址
说明:设置Samba Server是否使用别的wins服务器提供wins服务。

wins proxy = yes/no
说明:设置Samba Server是否开启wins代理服务。

dns proxy = yes/no
说明:设置Samba Server是否开启dns代理服务。

load printers = yes/no
说明:设置是否在启动Samba时就共享打印机。

printcap name = cups
说明:设置共享打印机的配置文件。

printing = cups
说明:设置Samba共享打印机的类型。现在支持的打印系统有:bsd, sysv, plp, lprng, aix, hpux, qnx

共享参数 [共享名]

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
58
59
60
61
62
63
64
65
66
[共享名]

comment = 任意字符串
说明:comment是对该共享的描述,可以是任意字符串。

path = 共享目录路径
说明:path用来指定共享目录的路径。可以用%u、%m这样的宏来代替路径里的unix用户和客户机的Netbios名,用宏表示主要用于[homes]共享域。例如:如果我们不打算用home段做为客户的共享,而是在/home/share/下为每个Linux用户以他的用户名建个目录,作为他的共享目录,这样path就可以写成:path = /home/share/%u; 。用户在连接到这共享时具体的路径会被他的用户名代替,要注意这个用户名路径一定要存在,否则,客户机在访问时会找不到网络路径。同样,如果我们不是以用户来划分目录,而是以客户机来划分目录,为网络上每台可以访问samba的机器都各自建个以它的netbios名的路径,作为不同机器的共享资源,就可以这样写:path = /home/share/%m 。

browseable = yes/no
说明:browseable用来指定该共享是否可以浏览。

writable = yes/no
说明:writable用来指定该共享路径是否可写。

available = yes/no
说明:available用来指定该共享资源是否可用。

admin users = 该共享的管理者
说明:admin users用来指定该共享的管理员(对该共享具有完全控制权限)。在samba 3.0中,如果用户验证方式设置成“security=share”时,此项无效。
例如:admin users =bobyuan,jane(多个用户中间用逗号隔开)。

valid users = 允许访问该共享的用户
说明:valid users用来指定允许访问该共享资源的用户。
例如:valid users = bobyuan,@bob,@tech(多个用户或者组中间用逗号隔开,如果要加入一个组就用“@+组名”表示。)

invalid users = 禁止访问该共享的用户
说明:invalid users用来指定不允许访问该共享资源的用户。
例如:invalid users = root,@bob(多个用户或者组中间用逗号隔开。)

write list = 允许写入该共享的用户
说明:write list用来指定可以在该共享下写入文件的用户。
例如:write list = bobyuan,@bob

public = yes/no
说明:public用来指定该共享是否允许guest账户访问。

guest ok = yes/no
说明:意义同“public”。

几个特殊共享:
[homes]
comment = Home Directories
browseable = no
writable = yes
valid users = %S
; valid users = MYDOMAIN\%S

[printers]
comment = All Printers
path = /var/spool/samba
browseable = no
guest ok = no
writable = no
printable = yes

[netlogon]
comment = Network Logon Service
path = /var/lib/samba/netlogon
guest ok = yes
writable = no
share modes = no

[Profiles]
path = /var/lib/samba/profiles
browseable = no
guest ok = yes

常见问题

你可能没有权限访问网络资源

问题现象:

  • 出现 NT_STATUS_ACCESS_DENIED 错误
  • Windows 下成功登陆 samba 后,点击共享目录仍然提示——你可能没有权限访问网络资源。

解决步骤:

  1. 检查是否配置了防火墙规则
1
2
3
4
5
6
# 一种方法是强行关闭防火墙
$ sudo service iptables stop

# 另一种方法是配置防火墙规则
$ sudo firewall-cmd --permanent --zone=public --add-service=samba
$ sudo firewall-cmd --reload
  1. 关闭 selinux
1
2
3
4
5
# 将 /etc/selinux/config 文件中的 SELINUX 设为 disabled
$ sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config

# 重启生效
$ reboot

window 下对 samba 的清理操作

  1. windows 清除访问 samba 局域网密码缓存
    • 在 dos 窗口中输入 control userpasswords2 或者 control keymgr.dll,然后【高级】/【密码管理】,删掉保存的该机器密码。
  2. windows 清除连接的 linux 的 samba 服务缓存
    1. 打开 win 的命令行。
    2. 输入 net use,就会打印出当前缓存的连接上列表。
    3. 根据列表,一个个删除连接: net use 远程连接名称 /del;或者一次性全部删除:net use * /del

参考资料

如何学习编程语言

前言

很多人喜欢争论什么什么编程语言好,我认为这个话题如果不限定应用范围,就毫无意义。

每种编程语言必然有其优点和缺点,这也决定了它有适合的应用场景和不适合的应用场景。现代软件行业,想一门编程语言包打天下是不现实的。这中现状也造成了一种现象,一个程序员往往要掌握多种编程语言。

学习任何一门编程语言,都会面临的第一个问题都是:如何学习 XX 语言?

我不想说什么多看、多学、多写、多练之类的废话。世上事有难易乎?无他,唯手熟尔。谁不知道熟能生巧的道理?

我觉得有必要谈谈的是:如何由浅入深的学习一门编程语言?学习所有编程语言有没有一个相对统一的学习方法?

曾几何时,当我还是一名小菜鸟时,总是叹服那些大神掌握多门编程语言。后来,在多年编程工作和学习中,我陆陆续续也接触过不少编程语言:C、C++、Java、C#、Javascript、shell 等等。每次学习一门新的编程语言,掌握程度或深或浅,但是学习的曲线却大抵相似。

下面,我按照个人的学习经验总结一下,学习编程语言的基本步骤。

学习编程语言的步骤

基本语法

首先当然是了解语言的最基本语法。

控制台输出,如 C 的 printf,Java 的 System.out.println 等。

普通程序员的第一行代码一般都是输出 “Hello World” 吧。

  • 基本数据类型

    不同编程语言的基本数据类型不同。基本数据类型是的申请内存空间变得方便、规范化。

  • 变量

    不同编程语言的声明变量方式有很大不同。有的如 Java 、C++ 需要明确指定变量数据类型,这种叫强类型定义语言。有的语言(主要是脚本语言),如 Javascript、Shell 等,不需要明确指定数据类型,这种叫弱类型定义语言。

    还需要注意的一点是变量的作用域范围和生命周期。不同语言变量的作用域范围和生命周期不一定一样,这个需要在代码中细细体会,有时会为此埋雷。

  • 逻辑控制语句

    编程语言都会有逻辑控制语句,哪怕是汇编语言。

    掌握条件语句、循环语句、中断循环语句(break、continue)、选择语句。一般区别仅仅在于关键字、语法格式略有不同。

  • 运算符

    掌握基本运算符,如算术运算符、关系运算符、逻辑运算符、赋值运算符等。

    有些语言还提供位运算符、特殊运算符,视情节掌握。

  • 注释(没啥好说的)

  • 函数

    编程语言基本都有函数。注意语法格式:是否支持出参;支持哪些数据作为入参,有些语言允许将函数作为参数传入另一个参数(即回调);返回值;如何退出函数(如 Java、C++的 return,)。

数组、枚举、集合

枚举只有部分编程语言有,如 Java、C++、C#。

但是数组和集合(有些语言叫容器)一般编程语言都有,只是有的编程语言提供的集合比较丰富。使用方法基本类似。

常用类

比较常用的类(当然有些语言中不叫类,叫对象或者其他什么,这个不重要,领会精神)请了解其 API 用法,如:字符串、日期、数学计算等等。

语言特性

语言特性这个特字反映的就是各个编程语言自身的”独特个性”,这涉及的点比较多,简单列举一些。

编程模式

比较流行的编程模式大概有:

面向对象编程,主要是封装、继承、多态;函数式编程,主要是应用 Lambda;过程式编程,可以理解为实现需求功能的特定步骤。

每种编程模式都有一定的道理,我从不认为只有面向对象编程才是王道。

Java 是面向对象语言,从 Java8 开始也支持函数编程(引入 Lambda 表达式);C++ 可以算是半面向对象,半面向过程式语言。

语言自身特性

每个语言自身都有一些重要特性需要了解。例如,学习 C、C++,你必须了解内存的申请和释放,了解指针、引用。而学习 Java,你需要了解 JVM,垃圾回收机制。学习 Javascript,你需要了解 DOM 操作等。

代码组织、模块加载、库管理

一个程序一般都有很多个源代码文件。这就会引入这些问题:如何将代码文件组织起来?如何根据业务需要,选择将部分模块启动时进行加载,部分模块使用懒加载(或者热加载)?

最基本的引用文件就不提了,如 C、C++的#include,Java 的 import 等。

针对代码组织、模块加载、库管理这些问题,不同语言会有不同的解决方案。

如 Java 可以用 maven、gradle 管理项目依赖、组织代码结构;Javascript (包括 Nodejs、jquery、react 等等库)可以用 npm、yarn 管理依赖,用 webpack 等工具管理模块加载。

容错处理

程序总难免会有 bug。

所以为了代码健壮性也好,为了方便定位问题也好,代码中需要有容错处理。常见的手段有:

  • 异常
  • 断言
  • 日志
  • 调试
  • 单元测试

输入输出和文件处理

这块知识比较繁杂。建议提纲挈领的学习一下,理解基本概念,比如输入输出流、管道等等。至于 API,用到的时候再查一下即可。

回调机制

每种语言实现回调的方式有所不同,如 .Net 的 delegate (大量被用于 WinForm 程序);Javascript 中函数天然支持回调:Javascript 函数允许传入另一个函数作为入参,然后在方法中调用它。其它语言的回调方式不一一列举。

序列化和反序列化

首先需要了解的是,序列化和反序列化的作用是为了在不同平台之间传输对象。

其次,要知道序列化存在多种方式,不同编程语言可能有多种方案。根据应用的序列化方式,选择性了解即可。

进阶特性

以下学习内容属于进阶性内容。可以根据开发需要去学习、掌握。需要注意的是,学习这些特性的态度应该是不学则已,学则死磕。因为半懂半不懂,特别容易引入问题。

对于半桶水的同学,我想说:放过自己,也放过别人,活着不好吗?

  • 并发编程:好处多多,十分重要,但是并发代码容易出错,且出错难以定位。要学习还是要花很大力气的,需要了解大量知识,如:进程、线程、同步、异步、读写锁等等。

  • 反射 - 让你可以动态编程(慎用)。

  • 泛型 - 集合(或者叫容器)的基石。精通泛型,能大大提高你的代码效率。

  • 元数据 - 描述数据的数据。Java 中叫做注解。

库和框架

学习一门编程语言,难免需要用到围绕它构建的技术生态圈——库和框架。这方面知识范围太庞大,根据实际应用领域去学习吧。比如搞 JavaWeb,你多多少少肯定要用到 Spring、Mybatis、Hibernate、Shiro 等大量开发框架;如果做 Javascript 前端,你可能会用到 React、Vue、Angular 、jQuery 等库或框架。

小结

总结以上,编程语言学习的道路是任重而道远的,未来是光明的。

最后一句话与君共勉:路漫漫兮其修远,吾将上下而求索。

Spring 4 升级踩雷指南

前言

最近,一直在为公司老项目做核心库升级工作。本来只是想升级一下 JDK8 ,却因为兼容性问题而不得不升级一些其他的库,而其他库本身依赖的一些库可能也要同步升级。这是一系列连锁问题,你很难一一识别,往往只有在编译时、运行时才能发现问题。

总之,这是个费劲的活啊。

本文小结一下升级 Spring4 的连锁问题。

为什么升级 spring4

升级 Spring4 的原因是:Spring 4 以前的版本不兼容 JDK8。当你的项目同时使用 Spring3 和 JDK8,如果代码中有使用 JDK8 字节码或 Lambada 表达式,那么会出问题。

也许你会问,为什么不使用最新的 Spring 5 呢?因为作为企业软件,一般更倾向使用稳定的版本(bug 少),而不是最新的版本,尤其是一些核心库。

更多细节可以参考:

https://spring.io/blog/2013/05/21/spring-framework-4-0-m1-3-2-3-available/

spring 4 重要新特性

Spring 4 相比 Spring 3,引入许多新特性,这里列举几条较为重要的:

  1. 支持 JDK8 (这个是最主要的)。
  2. Groovy Bean Definition DSL 风格配置。
  3. 支持 WebSocket、SockJS、STOMP 消息
  4. 移除 Deprecated 包和方法
  5. 一些功能加强,如:核心容器、Web、Test 等等,不一一列举。

更多 Spring 4 新特性可以参考:

https://docs.spring.io/spring/docs/4.3.14.BUILD-SNAPSHOT/spring-framework-reference/htmlsingle/#spring-whats-new

http://jinnianshilongnian.iteye.com/blog/1995111

升级 spring 4 步骤

了解了前面内容,我们知道了升级 Spring 4 带来的好处。现在开始真刀真枪的升级了。

不要以为升级一下 Spring 4,仅仅是改一下版本号,那么简单,细节处多着呢。

下面,结合我在公司项目升级 Spring4 时遇到的一系列坑,希望能帮助各位少走弯路。

下文内容基于假设你的项目是用 maven 管理这一前提。如果不满足这一前提,那么这篇文章对你没什么太大帮助。

修改 spring 版本

第一步,当然是修改 pom.xml 中的 spring 版本。

3.x.x.RELEASE > 4.x.x.RELEASE

实例:升级 spring-core

其它 spring 库的升级也如此:

1
2
3
4
5
6
7
8
<properties>
<spring.version>4.3.13.RELEASE</spring.version>
</properties>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>

修改 spring xml 文件的 xsd

用过 spring 的都知道,spring 通常依赖于大量的 xml 配置。

spring 的 xml 解析器在解析 xml 时,需要读取 xml schema,schema 定义了 xml 的命名空间。它的好处在于可以避免命名冲突,有点像 Java 中的 package。

实例:一个 spring xml 的 schema

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util" xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

说明

  • xmlns="http://www.springframework.org/schema/beans" 声明 xml 文件默认的命名空间,表示未使用其他命名空间的所有标签的默认命名空间。

  • xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 声明 XML Schema 实例,声明后就可以使用 schemaLocation 属性了。

  • xmlns:mvc="http://www.springframework.org/schema/mvc"
    声明前缀为 mvc 的命名空间,后面的 URL 用于标示命名空间的地址不会被解析器用于查找信息。其惟一的作用是赋予命名空间一个惟一的名称。当命名空间被定义在元素的开始标签中时,所有带有相同前缀的子元素都会与同一个命名空间相关联。 其它的类似 xmlns:contextxmlns:jdbc 等等同样如此。

  • xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
    ..."
    
    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

    这个从命名可以看出个大概,指定 schema 位置这个属性必须结合命名空间使用。这个属性有两个值,第一个值表示需要使用的命名空间。第二个值表示供命名空间使用的 xml schema 的位置。

    上面示例中的 xsd 版本是 `3.1.xsd` ,表示 spring 的 xml 解析器会将其视为 3.1 版本的 xml 文件来处理。

    现在,我们使用了 Spring 4,`3.1.xsd` 版本显然就不正确了,我们可以根据自己引入的 Spring 4 的子版本号将其改为 `4.x.xsd` 。

    但是,还有一种更好的做法:把这个指定 xsd 版本的关键字干掉,类似这样:`http://www.springframework.org/schema/tx/spring-tx.xsd` 。

    **这么做的原因如下:**

    - Spring 默认在启动时要加载 xsd 文件来验证 xml 文件。
    - 如果没有提供 `schemaLocation`,那么 spring 的 xml 解析器会从 namespace 的 uri 里加载 xsd 文件。
    - `schemaLocation` 提供了一个 xml namespace 到对应的 xsd 文件的一个映射。
    - 如果不指定 spring xsd 的版本号,spring 取的就是当前本地 jar 里的 xsd 文件,减少了各种风险(比如 xsd 与实际 spring jar 版本不一致)。

    更多详细内容可以参考这篇文章:[为什么在 Spring 的配置里,最好不要配置 xsd 文件的版本号](http://blog.csdn.net/hengyunabc/article/details/22295749)

    ### 修改 spring xml 文件

    spring 4xml 做了一些改动。这里说一个最常用的改动:

    #### ref local

    spring 不再支持 `ref` 元素的 `local` 属性,如果你的项目中使用了,需要改为 `bean`。

    shi

    spring 4 以前:

    ```xml
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource">
    <ref local="dataSource" />
    </property>
    </bean>

spring 4 以后:

1
2
3
4
5
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource">
<ref bean="dataSource" />
</property>
</bean>

如果不改启动会报错:

1
Caused by: org.xml.sax.SAXParseException: cvc-complex-type.3.2.2: Attribute 'local' is not allowed to appear in element 'ref'.

当然,可能还有一些其他配置改动,这个只能说兵来将挡水来土掩,遇到了再去查官方文档吧。

加入 spring support

spring 3 中很多的扩展内容不需要引入 support 。但是 spring 4 中分离的更彻底了,如果不分离,会有很多ClassNotFound

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.2.3.RELEASE</version>
</dependency>

更换 spring-mvc jackson

spring mvc 中如果返回结果为 json 需要依赖 jackson 的 jar 包,但是他升级到了 2, 以前是 codehaus.jackson,现在换成了 fasterxml.jackson

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.0</version>
</dependency>

同时修改 spring mvc 的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="stringHttpMessageConverter" />
<bean
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
</bean>
</list>
</property>
</bean>

<bean id="stringHttpMessageConverter"
class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
</list>
</property>
</bean>

解决 ibatis 兼容问题

问题

如果你的项目中使用了 ibatis (mybatis 的前身)这个 orm 框架,当 spring3 升级 spring4 后,会出现兼容性问题,编译都不能通过。

这是因为 Spring4 官方已经不再支持 ibatis。

解决方案

添加兼容性 jar 包

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-2-spring</artifactId>
<version>1.0.1</version>
</dependency>

更多内容可参考:https://stackoverflow.com/questions/32353286/no-support-for-ibatis-in-spring4-2-0

升级 Dubbo

我们的项目中使用了 soa 框架 Dubbo 。由于 Dubbo 是老版本的,具体来说是(2013 年的 2.4.10),而老版本中使用的 spirng 版本为 2.x,有兼容性问题。

Dubbo 项目从今年开始恢复维护了,首先把一些落后的库升级到较新版本,比如 jdk8,spring4 等,并修复了一些 bug。所以,我们可以通过升级一下 Dubbo 版本来解决问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.8</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
<exclusion>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
</exclusion>
</exclusions>
</dependency>

升级 Jedis

升级 Dubbo 为当前最新的 2.5.8 版本后,运行时报错:

  • JedisPoolConfig 配置错误
1
Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig

由于项目中使用了 redis,版本为 2.0.0 ,这个问题是由于 jedis 需要升级:

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

jedis 2.4.1 以上版本的 JedisPoolConfig 已经没有了maxActivemaxWait 属性。

修改方法如下:

maxActive > maxTotal

maxWait > maxWaitMillis

1
2
3
4
5
6
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="200" />
<property name="maxIdle" value="10" />
<property name="maxWaitMillis" value="1000" />
<property name="testOnBorrow" value="true" />
</bean>

JedisPool 配置错误

1
InvalidURIException: Cannot open Redis connection due invalid URI

原来的配置如下:

1
2
3
4
5
<bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="destroy" depends-on="jedisPoolConfig">
<constructor-arg ref="jedisPoolConfig" />
<constructor-arg type="java.lang.String" value="${redis.host}" />
<constructor-arg type="int" value="${redis.port}" />
</bean>

查看源码可以发现,初始化 JedisPool 时未指定结构方法参数的类型,导致 host 字符串值被视为 URI 类型,当然类型不匹配。

解决方法是修改上面的 host 配置,为:<constructor-arg type="java.lang.String" value="${redis.host}" />


至此,spring 4 升级结束。后面如果遇到其他升级问题再补充。

资料

Ant 简易教程

简介

Apache Ant 是一个将软件编译、测试、部署等步骤联系在一起加以自动化的一个工具,大多用于 Java 环境中的软件开发。由 Apache 软件基金会所提供。

Ant 是纯 Java 语言编写的,所以具有很好的跨平台性。

img

下载和安装

下载

ant 的官方下载地址:http://ant.apache.org/bindownload.cgi

进入页面后,在下图的红色方框中可以下载最新版本。笔者下载的版本是 apache-ant-1.9.4。

img

配置环境变量

配置环境变量(我的电脑 -> 属性 -> 高级 -> 环境变量)。

设置 ant 环境变量:

ANT_HOME C:/ apache-ant-1.9.4

img

**path ** C:/ apache-ant-1.9.4/bin

img

classpath C:/apache-ant-1.9.4/lib

img

验证

点击 开始 -> 运行 -> 输入 cmd

执行构建文件

输入如下命令:ant

如果出现如下内容,说明安装成功:

Buildfile: build.xml does not exist!
Build failed

注意:因为 ant 默认运行 build.xml 文件,这个文件需要我们创建。

如果不想命名为 build.xml,运行时可以使用 ant -buildfile test.xml 命令指明要运行的构建文件。

查看版本信息

输入 ant -version,可以查看版本信息。

img

但如果出现 ‘ant’ 不是内部或外部命令,也不是可运行的程序或批处理文件,说明安装失败:(可以重复前述步骤,直至安装成功。)

例子

在安装和配置成功后,我们就可以使用 ant 了。

为了让读者对 ant 有一个直观的认识,首先以 Ant 官方手册上的一个简单例子做一个说明。

以下是一个 build.xml 文件的内容:

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
<project name="MyProject" default="dist" basedir=".">
<description>
simple example build file
</description>
<!-- set global properties for this build -->
<property name="src" location="src"/>
<property name="build" location="build"/>
<property name="dist" location="dist"/>

<target name="init">
<!-- Create the time stamp -->
<tstamp/>
<!-- Create the build directory structure used by compile -->
<mkdir dir="${build}"/>
</target>

<target name="compile" depends="init"
description="compile the source " >
<!-- Compile the java code from ${src} into ${build} -->
<javac srcdir="${src}" destdir="${build}"/>
</target>

<target name="dist" depends="compile"
description="generate the distribution" >
<!-- Create the distribution directory -->
<mkdir dir="${dist}/lib"/>

<!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
<jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
</target>

<target name="clean"
description="clean up" >
<!-- Delete the ${build} and ${dist} directory trees -->
<delete dir="${build}"/>
<delete dir="${dist}"/>
</target>
</project>

在这个 xml 文件中,有几个 target 标签,每个 target 对应一个执行目标。

我们将这个 build.xml 放在 D:\Temp\ant_test 路径下,然后在 dos 界面下进行测试。

ant init

img

在 D:\Temp\ant_test 路径下创建了一个 build 目录,执行成功。

ant compile

img

提示错误,原来是在 build.xml 的所在目录下找不到 src 目录。好的,我们直接创建一个 src 目录,然后再次尝试。这次,执行成功。

img

**ant dist **

img

在 D:\Temp\ant_test 路径下创建了一个 dist 目录,执行成功。

ant clean

img

清除创建的 build 和 dist 目录,执行成功。

一个细节

细心的读者,想必已经发现一个问题——在执行 ant compile 和 ant dist 命令的时候把前面的命令也执行了。这是为什么呢?

请留意一下 build.xml 中的内容。有部分 target 标签中含有 depends 关键字。

img

这表明,当前的 target 在执行时需要依赖其他的 target,必须先执行依赖的 target,然后再执行。

关键元素

Ant 的构件文件都是 XML 格式的。每个构件文件包含一个 project 元素和至少一个 target。

target 元素可以包含多个 task 元素。

Project 元素

project 元素是构建文件的根元素。

一个 project 元素可以有多个 target 元素,一个 target 元素可以有多个 task。

在上节的例子中,project 标签里有三个属性。

1
<project name="MyProject" default="dist" basedir=".">

name 属性,指示 project 元素的名字。例子中的名字就是 MyProject。

default 属性,指示这个 project 默认执行的 target。在本文的例子中,默认执行的 target 为 dist。

如果我们输入命令 ant 时,不指定 target 参数,默认会执行 dist 这个 target。

basedir 属性,指定根路径的位置。该属性没有指定时,使用 Ant 的构件文件的所在目录作为根目录。

Target 元素

target 元素是 task 的容器,也就是 Ant 的一个基本执行单元。

以上节例子中的 compile 来举例。

1
2
3
4
<target name="compile" depends="init" description="compile the source " >
<!-- Compile the java code from ${src} into ${build} -->
<javac srcdir="${src}" destdir="${build}"/>
</target>

这个 target 中出现了几个属性。

name 属性,指示 target 元素的名称。

这个属性在一个 project 元素中必须是唯一的。这很好理解,如果出现重复,Ant 就不知道具体该执行哪个 target 了。

depends 属性,指示依赖的 target,当前的 target 必须在依赖的 target 之后执行。

description 属性,是关于 target 的简短说明。

此外,还有其他几个未出现在构建文件中的属性。

if 属性,验证指定的属性是否存在,若不存在,所在 target 将不会被执行。

unless 属性正好和 if 属性相反,验证指定的属性是否存在,若存在,所在 target 将不会被执行。****

extensionOf 属性,添加当前 target 到 extension-point 依赖列表。——Ant1.8.0 新特性。

extension-point 元素和 target 元素十分类似,都可以指定依赖的 target。但是不同的是,extension-point 中不能包含任何 task。

请看以下实例:

1
2
3
4
5
6
7
<target name="create-directory-layout">
...
</target>
<extension-point name="ready-to-compile" depends="create-directory-layout"/>
<target name="compile" depends="ready-to-compile">
...
</target>

调用 target 顺序: create-directory-layout –> ‘empty slot’ –> compile

1
2
3
<target name="generate-sources" extensionOf="ready-to-compile">
...
</target>

调用 target 顺序: create-directory-layout –> generate-sources –> compile

onMissingExtensionPoint 属性:当无法找到一个 extension-point 时,target 尝试去做的动作(“fail”, “warn”, “ignore”)。——Ant1.8.2 新特性

Task 元素

task 是一段可以被执行的代码。

一个 task 可以有多个属性, 一个属性可以包含对一个 property 的引用。

task 的通常结构为

1
<name attribute1="value1" attribute2="value2" ... />

其中,name 是 task 的名字, attributeN 是属性名, valueN 是这个属性的值。

还是以 compile 做为例子:

1
2
3
4
<target name="compile" depends="init" description="compile the source " >
<!-- Compile the java code from srcintosrcinto{build} -->
<javac srcdir="${src}" destdir="${build}"/>
</target>

在 compile 这个 target 标签中包含了一个任务。

这个任务的动作是:执行 JAVA 编译,编译 src 下的代码,并把编译生成的文件放在 build 目录中。

**常用 task **

javac:用于编译一个或者多个 Java 源文件,通常需要 srcdir 和 destdir 两个属性,用于指定 Java 源文件的位置和编译后 class 文件的保存位置。

1
<javac srcdir="${src}" destdir="${build}" classpath="abc.jar" debug="on" source="1.7" />

java:用于运行某个 Java 类,通常需要 classname 属性,用于指定需要运行哪个类。

1
2
3
4
5
6
<java classname="test.Main">
<arg value="-h" />
<classpath>
<pathelement location="dist/test.jar" />
</classpath>
</java>

jar:用于生成 JAR 包,通常需要指定 destfile 属性,用于指定所创建 JAR 包的文件名。除此之外,通常还应指定一个文件集,表明需要将哪些文件打包到 JAR 包里。

1
<jar jarfile="dist/lib/MyProject−dist/lib/MyProject−{DSTAMP}.jar" basedir="${build}"/>

echo:输出某个字符串。

1
2
<echo message="Building to ${builddir}"/>
<echo>You are using version ${java.version} of Java! This message spans two lines.</echo>

copy:用于复制文件或路径。

1
2
3
4
5
6
7
8
<copy todir="${builddir}/srccopy">
<fileset dir="${srcdir}">
<include name="**/*.java"/>
</fileset>
<filterset>
<filter token="VERSION" value="${app.version}"/>
</filterset>
</copy>

delete:用于删除文件或路径。

1
2
3
4
5
6
7
8
<copy todir="${builddir}/srccopy">
<fileset dir="${srcdir}">
<include name="**/*.java"/>
</fileset>
<filterset>
<filter token="VERSION" value="${app.version}"/>
</filterset>
</copy>

mkdir:用于创建文件夹。

1
<mkdir dir="${dist}/lib" />

move:用户移动文件和路径。

1
2
3
4
5
6
<move todir="some/new/dir">
<fileset dir="my/src/dir">
<include name="**/*.jar" />
<exclude name="**/ant.jar" />
</fileset>
</move>

Property 元素

Property 是对参数的定义。

project 的属性可以通过 property 元素来设定,也可在 Ant 之外设定。若要在外部引入某文件,例如 build.properties 文件,可以通过如下内容将其引入:<property file=” build.properties”/>。

property 元素可用作 task 的属性值。在 task 中是通过将属性名放在“${”和“}”之间,并放在 task 属性值的位置来实现的。

例如 complile 例子中,使用了前面定义的 src 作为源目录。

1
<javac srcdir="${src}" destdir="${build}"/>

Ant 提供了一些内置的属性,它能得到的系统属性的列表与 Java 文档中 System.getPropertis()方法得到的属性一致,这些系统属性可参考 sun 网站的说明。

extension-point 元素

和 target 元素十分类似,都可以指定依赖的 target。但是不同的是,extension-point 中不能包含任何 task。

——Ant1.8.0 新增特性。

在 target 元素中的例子里已提到过,不再赘述。

参考资料

软件工程入门指南

软件工程是一门研究用工程化方法构建和维护有效的、实用的和高质量的软件的学科。它涉及程序设计语言、数据库、软件开发工具、系统平台、标准、设计模式等方面。

软件工程的目标

软件工程的目标是:在给定成本、进度的前提下,开发出具有适用性、有效性、可修改性、可靠性、可理解性、可维护性、可重用性、可移植性、可追踪性、可互操作性和满足用户需求的软件产品。

  • 适用性 - 软件在不同的系统约束条件下,使用户需求得到满足的难易程度。
  • 有效性 - 软件系统能最有效的利用计算机的时间和空间资源。各种软件无不把系统的时/空开销作为衡量软件质量的一项重要技术指标。很多场合,在追求时间有效性和空间有效性时会发生矛盾,这时不得不牺牲时间有效性换取空间有效性或牺牲空间有效性换取时间有效性。时/空折衷是经常采用的技巧。
  • 可修改性 - 允许对系统进行修改而不增加原系统的复杂性。它支持软件的调试和维护,是一个难以达到的目标。
  • 可靠性 - 能防止因概念、设计和结构等方面的不完善造成的软件系统失效,具有挽回因操作不当造成软件系统失效的能力。
  • 可理解性 - 系统具有清晰的结构,能直接反映问题的需求。可理解性有助于控制系统软件复杂性,并支持软件的维护、移植或重用。
  • 可维护性 - 软件交付使用后,能够对它进行修改,以改正潜伏的错误,改进性能和其它属性,使软件产品适应环境的变化等。软件维护费用在软件开发费用中占有很大的比重。可维护性是软件工程中一项十分重要的目标。
  • 可重用性 - 把概念或功能相对独立的一个或一组相关模块定义为一个软部件。可组装在系统的任何位置,降低工作量。
  • 可移植性 - 软件从一个计算机系统或环境搬到另一个计算机系统或环境的难易程度。
  • 可追踪性 - 根据软件需求对软件设计、程序进行正向追踪,或根据软件设计、程序对软件需求的逆向追踪的能力。
  • 可互操作性 - 多个软件元素相互通信并协同完成任务的能力。

软件工程的原理

软件工程的七条基本原理:

  1. 用分阶段的生存周期计划进行严格的管理。
  2. 坚持进行阶段评审。
  3. 实行严格的产品控制。
  4. 采用现代程序设计技术。
  5. 软件工程结果应能清楚地审查。
  6. 开发小组的人员应该少而精。
  7. 承认不断改进软件工程实践的必要性。

软件工程的方法

著名的重量级开发方法:

  • ISO9000 - ISO 9000 系列标准是国际标准化组织设立的标准,与品质管理系统有关。
  • 能力成熟度模型(CMM) - CMM 涵盖一个成熟的软件发展组织所应具备的重要功能与项目,它描述了软件发展的演进过程,从毫无章法、不成熟的软件开发阶段到成熟软件开发阶段的过程。
  • 统一软件开发过程(RUP) - RUP 是一种软件工程方法,为迭代式软件开发流程。

著名的轻量级开发方法:

  • 敏捷开发(Agile Development) - 是一种应对快速变化的需求的一种软件开发能力。它们的具体名称、理念、过程、术语都不尽相同,相对于“非敏捷”,更强调程序员团队与业务专家之间的紧密协作、面对面的沟通(认为比书面的文档更有效)、频繁交付新的软件版本、紧凑而自我组织型的团队、能够很好地适应需求变化的代码编写和团队组织方法,也更注重软件开发过程中人的作用。
  • 极限编程(XP) - 极限编程是敏捷软件开发中最有成效的方法学之一。极限编程技术以沟通(Communication)、简单(Simplicity)、反馈(Feedback)、勇气(Courage)和尊重(Respect)为价值标准。

软件需求

软件需求包括三个不同的层次:业务需求、用户需求和功能需求。

  • 业务需求(Business requirement)表示组织或客户高层次的目标。业务需求通常来自项目投资人、购买产品的客户、实际用户的管理者、市场营销部门或产品策划部门。业务需求描述了组织为什么要开发一个系统,即组织希望达到的目标。使用前景和范围( vision and scope )文档来记录业务需求,这份文档有时也被称作项目轮廓图或市场需求( project charter 或 market requirement )文档。

  • 用户需求(user requirement)描述的是用户的目标,或用户要求系统必须能完成的任务。用例、场景描述和事件――响应表都是表达用户需求的有效途径。也就是说用户需求描述了用户能使用系统来做些什么。

  • 功能需求(functional requirement)规定开发人员必须在产品中实现的软件功能,用户利用这些功能来完成任务,满足业务需求。功能需求有时也被称作行为需求( behavioral requirement ),因为习惯上总是用“应该”对其进行描述:“系统应该发送电子邮件来通知用户已接受其预定”。功能需求描述是开发人员需要实现什么。

  • 系统需求(system requirement)用于描述包含多个子系统的产品(即系统)的顶级需求。系统可以只包含软件系统,也可以既包含软件又包含硬件子系统。人也可以是系统的一部分,因此某些系统功能可能要由人来承担。

软件需求说明书( SRS )

软件需求说明书( SRS )完整地描述了软件系统的预期特性。开发、测试、质量保证、项目管理和其他相关的项目功能都要用到 SRS 。

除了功能需求外, SRS 中还包含非功能需求,包括性能指标和对质量属性的描述。

  • 质量属性(quality attribute)对产品的功能描述作了补充,它从不同方面描述了产品的各种特性。这些特性包括可用性、可移植性、完整性、效率和健壮性,它们对用户或开发人员都很重要。其他的非功能需求包括系统与外部世界的外部界面,以及对设计与实现的约束。
  • 约束(constraint)限制了开发人员设计和构建系统时的选择范围。

软件生命周期

软件生命周期(Software Life Cycle,SLC)是软件的产生直到报废或停止使用的生命周期。

  • 问题定义 - 要求系统分析员与用户进行交流,弄清“用户需要计算机解决什么问题”然后提出关于“系统目标与范围的说明”,提交用户审查和确认。
  • 可行性研究 - 一方面在于把待开发的系统的目标以明确的语言描述出来;另一方面从经济、技术、法律等多方面进行可行性分析。
  • 需求分析 - 弄清用户对软件系统的全部需求,编写需求规格说明书和初步的用户手册,提交评审。
  • 开发阶段
    • 概要设计
    • 详细设计
    • 编码实现
    • 软件测试 - 测试的过程分单元测试、组装测试以及系统测试三个阶段进行。测试的方法主要有白盒测试和黑盒测试两种。
  • 维护

软件生命周期

软件生命周期模型

瀑布模型

瀑布模型(Waterfall Model)强调系统开发应有完整的周期,且必须完整的经历周期的每一开发阶段,并系统化的考量分析与设计的技术、时间与资源之投入等。

瀑布模型

瀑布模型思想

瀑布模型核心思想是按工序将问题拆分,将功能的实现与设计分开,便于分工协作,即采用结构化的分析与设计方法将逻辑实现与物理实现分开。将软件生命周期划分为制定计划、需求分析、软件设计、程序编写、软件测试和运行维护等六个基本活动,并且规定了它们自上而下、相互衔接的固定次序,如同瀑布流水,逐级下落。

瀑布模型特点

优点:

  • 为项目提供了按阶段划分的检查点。
  • 当前一阶段完成后,您只需要去关注后续阶段。
  • 可在迭代模型中应用瀑布模型。
  • 它提供了一个模板,这个模板使得分析、设计、编码、测试和支持的方法可以在该模板下有一个共同的指导。

缺点:

  • 各个阶段的划分完全固定,阶段之间产生大量的文档,极大地增加了工作量。
  • 由于开发模型是线性的,用户只有等到整个过程的末期才能见到开发成果,从而增加了开发风险。
  • 通过过多的强制完成日期和里程碑来跟踪各个项目阶段。
  • 瀑布模型的突出缺点是不适应用户需求的变化。

适用场景:

是否使用这一模型主要取决于是否能理解客户的需求以及在项目的进程中这些需求的变化程度。对于需求经常变化的项目,不要适用瀑布模型。

螺旋模型

螺旋模型基本做法是在“瀑布模型”的每一个开发阶段前引入一个非常严格的风险识别、风险分析和风险控制,它把软件项目分解成一个个小项目。每个小项目都标识一个或多个主要风险,直到所有的主要风险因素都被确定。

螺旋模型

螺旋模型思想

螺旋模型沿着螺线进行若干次迭代,图中的四个象限代表了以下活动:

  1. 制定计划 - 确定软件目标,选定实施方案,弄清项目开发的限制条件;
  2. 风险分析 - 分析评估所选方案,考虑如何识别和消除风险;
  3. 实施工程 - 实施软件开发和验证;
  4. 客户评估 - 评价开发工作,提出修正建议,制定下一步计划。

螺旋模型由风险驱动,强调可选方案和约束条件从而支持软件的重用,有助于将软件质量作为特殊目标融入产品开发之中。

螺旋模型特点

优点:

  • 设计上的灵活性,可以在项目的各个阶段进行变更。
  • 以小的分段来构建大型系统,使成本计算变得简单容易。
  • 客户始终参与每个阶段的开发,保证了项目不偏离正确方向以及项目的可控性。
  • 随着项目推进,客户始终掌握项目的最新信息, 从而他或她能够和管理层有效地交互。
  • 客户认可这种公司内部的开发方式带来的良好的沟通和高质量的产品。

缺点:

很难让用户确信这种演化方法的结果是可以控制的。建设周期长,而软件技术发展比较快,所以经常出现软件开发完毕后,和当前的技术水平有了较大的差距,无法满足当前用户需求。

适用场景:

对于新项目,需求不明确的情况下,适合用螺旋模型进行开发,便于风险控制和需求变更。

软件工程术语

  • 里程碑(Milestone) - 在制定项目进度计划时,在进度时间表上设立一些重要的时间检查点,这样一来,就可以在项目执行过程中利用这些重要的时间检查点来对项目的进程进行检查和控制。这些重要的时间检查点被称作项目的里程碑。
  • 人月 - 软件开发的工作量单位。如 200 人月,10 个人开发,那算来就是花 20 个月就可完工。
  • 基线 - 基线是项目储存库中每个工件版本在特定时期的一个“快照”。它提供一个正式标准,随后的工作基于此标准,并且只有经过授权后才能变更这个标准。建立一个初始基线后,以后每次对其进行的变更都将记录为一个差值,直到建成下一个基线。

资源

一篇文章让你彻底掌握 Shell

由于 bash 是 Linux 标准默认的 shell 解释器,可以说 bash 是 shell 编程的基础。

_本文主要介绍 bash 的语法,对于 linux 指令不做任何介绍_。

💻 本文的源码已归档到“ linux-tutorial

1
2
3
4
5
███████╗██╗  ██╗███████╗██╗     ██╗
██╔════╝██║ ██║██╔════╝██║ ██║
███████╗███████║█████╗ ██║ ██║
╚════██║██╔══██║██╔══╝ ██║ ██║
███████║██║ ██║███████╗███████╗███████╗

简介

什么是 shell

  • Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。
  • Shell 既是一种命令语言,又是一种程序设计语言。
  • Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问 Linux 内核的服务。

Ken Thompson 的 sh 是第一种 Unix Shell,Windows Explorer 是一个典型的图形界面 Shell。

什么是 shell 脚本

Shell 脚本(shell script),是一种为 shell 编写的脚本程序,一般文件后缀为 .sh

业界所说的 shell 通常都是指 shell 脚本,但 shell 和 shell script 是两个不同的概念。

Shell 环境

Shell 编程跟 java、php 编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。

Shell 的解释器种类众多,常见的有:

  • sh - 即 Bourne Shell。sh 是 Unix 标准默认的 shell。
  • bash - 即 Bourne Again Shell。bash 是 Linux 标准默认的 shell。
  • fish - 智能和用户友好的命令行 shell。
  • xiki - 使 shell 控制台更友好,更强大。
  • zsh - 功能强大的 shell 与脚本语言。

指定脚本解释器

在 shell 脚本,#! 告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 解释器。#! 被称作shebang(也称为 Hashbang )

所以,你应该会在 shell 中,见到诸如以下的注释:

  • 指定 sh 解释器
1
#!/bin/sh
  • 指定 bash 解释器
1
#!/bin/bash

注意

上面的指定解释器的方式是比较常见的,但有时候,你可能也会看到下面的方式:

1
#!/usr/bin/env bash

这样做的好处是,系统会自动在 PATH 环境变量中查找你指定的程序(本例中的bash)。相比第一种写法,你应该尽量用这种写法,因为程序的路径是不确定的。这样写还有一个好处,操作系统的PATH变量有可能被配置为指向程序的另一个版本。比如,安装完新版本的bash,我们可能将其路径添加到PATH中,来“隐藏”老版本。如果直接用#!/bin/bash,那么系统会选择老版本的bash来执行脚本,如果用#!/usr/bin/env bash,则会使用新版本。

模式

shell 有交互和非交互两种模式。

交互模式

简单来说,你可以将 shell 的交互模式理解为执行命令行。

看到形如下面的东西,说明 shell 处于交互模式下:

1
user@host:~$

接着,便可以输入一系列 Linux 命令,比如 lsgrepcdmkdirrm 等等。

非交互模式

简单来说,你可以将 shell 的非交互模式理解为执行 shell 脚本。

在非交互模式下,shell 从文件或者管道中读取命令并执行。

当 shell 解释器执行完文件中的最后一个命令,shell 进程终止,并回到父进程。

可以使用下面的命令让 shell 以非交互模式运行:

1
2
3
4
sh /path/to/script.sh
bash /path/to/script.sh
source /path/to/script.sh
./path/to/script.sh

上面的例子中,script.sh是一个包含 shell 解释器可以识别并执行的命令的普通文本文件,shbash是 shell 解释器程序。你可以使用任何喜欢的编辑器创建script.sh(vim,nano,Sublime Text, Atom 等等)。

其中,source /path/to/script.sh./path/to/script.sh 是等价的。

除此之外,你还可以通过chmod命令给文件添加可执行的权限,来直接执行脚本文件:

1
2
chmod +x /path/to/script.sh #使脚本具有执行权限
/path/to/test.sh

这种方式要求脚本文件的第一行必须指明运行该脚本的程序,比如:

💻 “示例源码”

1
2
#!/usr/bin/env bash
echo "Hello, world!"

上面的例子中,我们使用了一个很有用的命令echo来输出字符串到屏幕上。

基本语法

解释器

前面虽然两次提到了#! ,但是本着重要的事情说三遍的精神,这里再强调一遍:

在 shell 脚本,#! 告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 解释器。#! 被称作shebang(也称为 Hashbang )

#! 决定了脚本可以像一个独立的可执行文件一样执行,而不用在终端之前输入sh, bash, python, php等。

1
2
3
# 以下两种方式都可以指定 shell 解释器为 bash,第二种方式更好
#!/bin/bash
#!/usr/bin/env bash

注释

注释可以说明你的代码是什么作用,以及为什么这样写。

shell 语法中,注释是特殊的语句,会被 shell 解释器忽略。

  • 单行注释 - 以 # 开头,到行尾结束。
  • 多行注释 - 以 :<<EOF 开头,到 EOF 结束。

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#--------------------------------------------
# shell 注释示例
# author:zp
#--------------------------------------------

# echo '这是单行注释'

########## 这是分割线 ##########

:<<EOF
echo '这是多行注释'
echo '这是多行注释'
echo '这是多行注释'
EOF

echo

echo 用于字符串的输出。

输出普通字符串:

1
2
echo "hello, world"
# Output: hello, world

输出含变量的字符串:

1
2
echo "hello, \"zp\""
# Output: hello, "zp"

输出含变量的字符串:

1
2
3
name=zp
echo "hello, \"${name}\""
# Output: hello, "zp"

输出含换行符的字符串:

1
2
3
4
5
6
7
8
# 输出含换行符的字符串
echo "YES\nNO"
# Output: YES\nNO

echo -e "YES\nNO" # -e 开启转义
# Output:
# YES
# NO

输出含不换行符的字符串:

1
2
3
4
5
6
7
8
9
10
echo "YES"
echo "NO"
# Output:
# YES
# NO

echo -e "YES\c" # -e 开启转义 \c 不换行
echo "NO"
# Output:
# YESNO

输出重定向至文件

1
echo "test" > test.txt

输出执行结果

1
2
echo `pwd`
# Output:(当前目录路径)

💻 “示例源码”

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
#!/usr/bin/env bash

# 输出普通字符串
echo "hello, world"
# Output: hello, world

# 输出含变量的字符串
echo "hello, \"zp\""
# Output: hello, "zp"

# 输出含变量的字符串
name=zp
echo "hello, \"${name}\""
# Output: hello, "zp"

# 输出含换行符的字符串
echo "YES\nNO"
# Output: YES\nNO
echo -e "YES\nNO" # -e 开启转义
# Output:
# YES
# NO

# 输出含不换行符的字符串
echo "YES"
echo "NO"
# Output:
# YES
# NO

echo -e "YES\c" # -e 开启转义 \c 不换行
echo "NO"
# Output:
# YESNO

# 输出内容定向至文件
echo "test" > test.txt

# 输出执行结果
echo `pwd`
# Output:(当前目录路径)

printf

printf 用于格式化输出字符串。

默认,printf 不会像 echo 一样自动添加换行符,如果需要换行可以手动添加 \n

💻 “示例源码”

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
# 单引号
printf '%d %s\n' 1 "abc"
# Output:1 abc

# 双引号
printf "%d %s\n" 1 "abc"
# Output:1 abc

# 无引号
printf %s abcdef
# Output: abcdef(并不会换行)

# 格式只指定了一个参数,但多出的参数仍然会按照该格式输出
printf "%s\n" abc def
# Output:
# abc
# def

printf "%s %s %s\n" a b c d e f g h i j
# Output:
# a b c
# d e f
# g h i
# j

# 如果没有参数,那么 %s 用 NULL 代替,%d 用 0 代替
printf "%s and %d \n"
# Output:
# and 0

# 格式化输出
printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg
printf "%-10s %-8s %-4.2f\n" 郭靖 男 66.1234
printf "%-10s %-8s %-4.2f\n" 杨过 男 48.6543
printf "%-10s %-8s %-4.2f\n" 郭芙 女 47.9876
# Output:
# 姓名 性别 体重kg
# 郭靖 男 66.12
# 杨过 男 48.65
# 郭芙 女 47.99

printf 的转义符

序列 说明
\a 警告字符,通常为 ASCII 的 BEL 字符
\b 后退
\c 抑制(不显示)输出结果中任何结尾的换行字符(只在%b 格式指示符控制下的参数字符串中有效),而且,任何留在参数里的字符、任何接下来的参数以及任何留在格式字符串中的字符,都被忽略
\f 换页(formfeed)
\n 换行
\r 回车(Carriage return)
\t 水平制表符
\v 垂直制表符
\\ 一个字面上的反斜杠字符
\ddd 表示 1 到 3 位数八进制值的字符。仅在格式字符串中有效
\0ddd 表示 1 到 3 位的八进制值字符

变量

跟许多程序设计语言一样,你可以在 bash 中创建变量。

Bash 中没有数据类型,bash 中的变量可以保存一个数字、一个字符、一个字符串等等。同时无需提前声明变量,给变量赋值会直接创建变量。

变量命名原则

  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
  • 中间不能有空格,可以使用下划线(_)。
  • 不能使用标点符号。
  • 不能使用 bash 里的关键字(可用 help 命令查看保留关键字)。

声明变量

访问变量的语法形式为:${var}$var

变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,所以推荐加花括号。

1
2
3
word="hello"
echo ${word}
# Output: hello

只读变量

使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

1
2
3
4
rword="hello"
echo ${rword}
readonly rword
# rword="bye" # 如果放开注释,执行时会报错

删除变量

使用 unset 命令可以删除变量。变量被删除后不能再次使用。unset 命令不能删除只读变量。

1
2
3
4
5
6
7
dword="hello"  # 声明变量
echo ${dword} # 输出变量值
# Output: hello

unset dword # 删除变量
echo ${dword}
# Output: (空)

变量类型

  • 局部变量 - 局部变量是仅在某个脚本内部有效的变量。它们不能被其他的程序和脚本访问。
  • 环境变量 - 环境变量是对当前 shell 会话内所有的程序或脚本都可见的变量。创建它们跟创建局部变量类似,但使用的是 export 关键字,shell 脚本也可以定义环境变量。

常见的环境变量:

变量 描述
$HOME 当前用户的用户目录
$PATH 用分号分隔的目录列表,shell 会到这些目录中查找命令
$PWD 当前工作目录
$RANDOM 0 到 32767 之间的整数
$UID 数值类型,当前用户的用户 ID
$PS1 主要系统输入提示符
$PS2 次要系统输入提示符

这里 有一张更全面的 Bash 环境变量列表。

💻 “示例源码”

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
#!/usr/bin/env bash

################### 声明变量 ###################
name="world"
echo "hello ${name}"
# Output: hello world

################### 输出变量 ###################
folder=$(pwd)
echo "current path: ${folder}"

################### 只读变量 ###################
rword="hello"
echo ${rword}
# Output: hello
readonly rword
# rword="bye" # 如果放开注释,执行时会报错

################### 删除变量 ###################
dword="hello" # 声明变量
echo ${dword} # 输出变量值
# Output: hello

unset dword # 删除变量
echo ${dword}
# Output: (空)

################### 系统变量 ###################
echo "UID:$UID"
echo LOGNAME:$LOGNAME
echo User:$USER
echo HOME:$HOME
echo PATH:$PATH
echo HOSTNAME:$HOSTNAME
echo SHELL:$SHELL
echo LANG:$LANG

################### 自定义变量 ###################
days=10
user="admin"
echo "$user logged in $days days age"
days=5
user="root"
echo "$user logged in $days days age"
# Output:
# admin logged in 10 days age
# root logged in 5 days age

################### 从变量读取列表 ###################
colors="Red Yellow Blue"
colors=$colors" White Black"

for color in $colors
do
echo " $color"
done

字符串

单引号和双引号

shell 字符串可以用单引号 '',也可以用双引号 “”,也可以不用引号。

  • 单引号的特点
    • 单引号里不识别变量
    • 单引号里不能出现单独的单引号(使用转义符也不行),但可成对出现,作为字符串拼接使用。
  • 双引号的特点
    • 双引号里识别变量
    • 双引号里可以出现转义字符

综上,推荐使用双引号。

拼接字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用单引号拼接
name1='white'
str1='hello, '${name1}''
str2='hello, ${name1}'
echo ${str1}_${str2}
# Output:
# hello, white_hello, ${name1}

# 使用双引号拼接
name2="black"
str3="hello, "${name2}""
str4="hello, ${name2}"
echo ${str3}_${str4}
# Output:
# hello, black_hello, black

获取字符串长度

1
2
3
4
text="12345"
echo ${#text}
# Output:
# 5

截取子字符串

1
2
3
4
text="12345"
echo ${text:2:2}
# Output:
# 34

从第 3 个字符开始,截取 2 个字符

查找子字符串

1
2
3
4
5
6
7
8
#!/usr/bin/env bash

text="hello"
echo `expr index "${text}" ll`

# Execute: ./str-demo5.sh
# Output:
# 3

查找 ll 子字符在 hello 字符串中的起始位置。

💻 “示例源码”

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#!/usr/bin/env bash

################### 使用单引号拼接字符串 ###################
name1='white'
str1='hello, '${name1}''
str2='hello, ${name1}'
echo ${str1}_${str2}
# Output:
# hello, white_hello, ${name1}

################### 使用双引号拼接字符串 ###################
name2="black"
str3="hello, "${name2}""
str4="hello, ${name2}"
echo ${str3}_${str4}
# Output:
# hello, black_hello, black

################### 获取字符串长度 ###################
text="12345"
echo "${text} length is: ${#text}"
# Output:
# 12345 length is: 5

# 获取子字符串
text="12345"
echo ${text:2:2}
# Output:
# 34

################### 查找子字符串 ###################
text="hello"
echo `expr index "${text}" ll`
# Output:
# 3

################### 判断字符串中是否包含子字符串 ###################
result=$(echo "${str}" | grep "feature/")
if [[ "$result" != "" ]]; then
echo "feature/ 是 ${str} 的子字符串"
else
echo "feature/ 不是 ${str} 的子字符串"
fi

################### 截取关键字左边内容 ###################
full_branch="feature/1.0.0"
branch=`echo ${full_branch#feature/}`
echo "branch is ${branch}"

################### 截取关键字右边内容 ###################
full_version="0.0.1-SNAPSHOT"
version=`echo ${full_version%-SNAPSHOT}`
echo "version is ${version}"

################### 字符串分割成数组 ###################
str="0.0.0.1"
OLD_IFS="$IFS"
IFS="."
array=( ${str} )
IFS="$OLD_IFS"
size=${#array[*]}
lastIndex=`expr ${size} - 1`
echo "数组长度:${size}"
echo "最后一个数组元素:${array[${lastIndex}]}"
for item in ${array[@]}
do
echo "$item"
done

################### 判断字符串是否为空 ###################
#-n 判断长度是否非零
#-z 判断长度是否为零

str=testing
str2=''
if [[ -n "$str" ]]
then
echo "The string $str is not empty"
else
echo "The string $str is empty"
fi

if [[ -n "$str2" ]]
then
echo "The string $str2 is not empty"
else
echo "The string $str2 is empty"
fi

# Output:
# The string testing is not empty
# The string is empty

################### 字符串比较 ###################
str=hello
str2=world
if [[ $str = "hello" ]]; then
echo "str equals hello"
else
echo "str not equals hello"
fi

if [[ $str2 = "hello" ]]; then
echo "str2 equals hello"
else
echo "str2 not equals hello"
fi

数组

bash 只支持一维数组。

数组下标从 0 开始,下标可以是整数或算术表达式,其值应大于或等于 0。

创建数组

1
2
3
# 创建数组的不同方式
nums=([2]=2 [0]=0 [1]=1)
colors=(red yellow "dark blue")

访问数组元素

  • 访问数组的单个元素:
1
2
echo ${nums[1]}
# Output: 1
  • 访问数组的所有元素:
1
2
3
4
5
echo ${colors[*]}
# Output: red yellow dark blue

echo ${colors[@]}
# Output: red yellow dark blue

上面两行有很重要(也很微妙)的区别:

为了将数组中每个元素单独一行输出,我们用 printf 命令:

1
2
3
4
5
6
printf "+ %s\n" ${colors[*]}
# Output:
# + red
# + yellow
# + dark
# + blue

为什么darkblue各占了一行?尝试用引号包起来:

1
2
3
printf "+ %s\n" "${colors[*]}"
# Output:
# + red yellow dark blue

现在所有的元素都在一行输出 —— 这不是我们想要的!让我们试试${colors[@]}

1
2
3
4
5
printf "+ %s\n" "${colors[@]}"
# Output:
# + red
# + yellow
# + dark blue

在引号内,${colors[@]}将数组中的每个元素扩展为一个单独的参数;数组元素中的空格得以保留。

  • 访问数组的部分元素:
1
2
3
echo ${nums[@]:0:2}
# Output:
# 0 1

在上面的例子中,${array[@]} 扩展为整个数组,:0:2取出了数组中从 0 开始,长度为 2 的元素。

访问数组长度

1
2
3
echo ${#nums[*]}
# Output:
# 3

向数组中添加元素

向数组中添加元素也非常简单:

1
2
3
4
colors=(white "${colors[@]}" green black)
echo ${colors[@]}
# Output:
# white red yellow dark blue green black

上面的例子中,${colors[@]} 扩展为整个数组,并被置换到复合赋值语句中,接着,对数组colors的赋值覆盖了它原来的值。

从数组中删除元素

unset命令来从数组中删除一个元素:

1
2
3
4
unset nums[0]
echo ${nums[@]}
# Output:
# 1 2

💻 “示例源码”

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
#!/usr/bin/env bash

################### 创建数组 ###################
nums=( [ 2 ] = 2 [ 0 ] = 0 [ 1 ] = 1 )
colors=( red yellow "dark blue" )

################### 访问数组的单个元素 ###################
echo ${nums[1]}
# Output: 1

################### 访问数组的所有元素 ###################
echo ${colors[*]}
# Output: red yellow dark blue

echo ${colors[@]}
# Output: red yellow dark blue

printf "+ %s\n" ${colors[*]}
# Output:
# + red
# + yellow
# + dark
# + blue

printf "+ %s\n" "${colors[*]}"
# Output:
# + red yellow dark blue

printf "+ %s\n" "${colors[@]}"
# Output:
# + red
# + yellow
# + dark blue

################### 访问数组的部分元素 ###################
echo ${nums[@]:0:2}
# Output:
# 0 1

################### 获取数组长度 ###################
echo ${#nums[*]}
# Output:
# 3

################### 向数组中添加元素 ###################
colors=( white "${colors[@]}" green black )
echo ${colors[@]}
# Output:
# white red yellow dark blue green black

################### 从数组中删除元素 ###################
unset nums[ 0 ]
echo ${nums[@]}
# Output:
# 1 2

运算符

算术运算符

下表列出了常用的算术运算符,假定变量 x 为 10,变量 y 为 20:

运算符 说明 举例
+ 加法 expr $x + $y 结果为 30。
- 减法 expr $x - $y 结果为 -10。
* 乘法 expr $x * $y 结果为 200。
/ 除法 expr $y / $x 结果为 2。
% 取余 expr $y % $x 结果为 0。
= 赋值 x=$y 将把变量 y 的值赋给 x。
== 相等。用于比较两个数字,相同则返回 true。 [ $x == $y ] 返回 false。
!= 不相等。用于比较两个数字,不相同则返回 true。 [ $x != $y ] 返回 true。

注意:条件表达式要放在方括号之间,并且要有空格,例如: [$x==$y] 是错误的,必须写成 [ $x == $y ]

💻 “示例源码”

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
x=10
y=20

echo "x=${x}, y=${y}"

val=`expr ${x} + ${y}`
echo "${x} + ${y} = $val"

val=`expr ${x} - ${y}`
echo "${x} - ${y} = $val"

val=`expr ${x} \* ${y}`
echo "${x} * ${y} = $val"

val=`expr ${y} / ${x}`
echo "${y} / ${x} = $val"

val=`expr ${y} % ${x}`
echo "${y} % ${x} = $val"

if [[ ${x} == ${y} ]]
then
echo "${x} = ${y}"
fi
if [[ ${x} != ${y} ]]
then
echo "${x} != ${y}"
fi

# Output:
# x=10, y=20
# 10 + 20 = 30
# 10 - 20 = -10
# 10 * 20 = 200
# 20 / 10 = 2
# 20 % 10 = 0
# 10 != 20

关系运算符

关系运算符只支持数字,不支持字符串,除非字符串的值是数字。

下表列出了常用的关系运算符,假定变量 x 为 10,变量 y 为 20:

运算符 说明 举例
-eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ]返回 false。
-ne 检测两个数是否相等,不相等返回 true。 [ $a -ne $b ] 返回 true。
-gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。
-lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。
-ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。
-le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ $a -le $b ]返回 true。

💻 “示例源码”

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
x=10
y=20

echo "x=${x}, y=${y}"

if [[ ${x} -eq ${y} ]]; then
echo "${x} -eq ${y} : x 等于 y"
else
echo "${x} -eq ${y}: x 不等于 y"
fi

if [[ ${x} -ne ${y} ]]; then
echo "${x} -ne ${y}: x 不等于 y"
else
echo "${x} -ne ${y}: x 等于 y"
fi

if [[ ${x} -gt ${y} ]]; then
echo "${x} -gt ${y}: x 大于 y"
else
echo "${x} -gt ${y}: x 不大于 y"
fi

if [[ ${x} -lt ${y} ]]; then
echo "${x} -lt ${y}: x 小于 y"
else
echo "${x} -lt ${y}: x 不小于 y"
fi

if [[ ${x} -ge ${y} ]]; then
echo "${x} -ge ${y}: x 大于或等于 y"
else
echo "${x} -ge ${y}: x 小于 y"
fi

if [[ ${x} -le ${y} ]]; then
echo "${x} -le ${y}: x 小于或等于 y"
else
echo "${x} -le ${y}: x 大于 y"
fi

# Output:
# x=10, y=20
# 10 -eq 20: x 不等于 y
# 10 -ne 20: x 不等于 y
# 10 -gt 20: x 不大于 y
# 10 -lt 20: x 小于 y
# 10 -ge 20: x 小于 y
# 10 -le 20: x 小于或等于 y

布尔运算符

下表列出了常用的布尔运算符,假定变量 x 为 10,变量 y 为 20:

运算符 说明 举例
! 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
-o 或运算,有一个表达式为 true 则返回 true。 [ $a -lt 20 -o $b -gt 100 ] 返回 true。
-a 与运算,两个表达式都为 true 才返回 true。 [ $a -lt 20 -a $b -gt 100 ] 返回 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
x=10
y=20

echo "x=${x}, y=${y}"

if [[ ${x} != ${y} ]]; then
echo "${x} != ${y} : x 不等于 y"
else
echo "${x} != ${y}: x 等于 y"
fi

if [[ ${x} -lt 100 && ${y} -gt 15 ]]; then
echo "${x} 小于 100 且 ${y} 大于 15 : 返回 true"
else
echo "${x} 小于 100 且 ${y} 大于 15 : 返回 false"
fi

if [[ ${x} -lt 100 || ${y} -gt 100 ]]; then
echo "${x} 小于 100 或 ${y} 大于 100 : 返回 true"
else
echo "${x} 小于 100 或 ${y} 大于 100 : 返回 false"
fi

if [[ ${x} -lt 5 || ${y} -gt 100 ]]; then
echo "${x} 小于 5 或 ${y} 大于 100 : 返回 true"
else
echo "${x} 小于 5 或 ${y} 大于 100 : 返回 false"
fi

# Output:
# x=10, y=20
# 10 != 20 : x 不等于 y
# 10 小于 100 且 20 大于 15 : 返回 true
# 10 小于 100 或 20 大于 100 : 返回 true
# 10 小于 5 或 20 大于 100 : 返回 false

逻辑运算符

以下介绍 Shell 的逻辑运算符,假定变量 x 为 10,变量 y 为 20:

运算符 说明 举例
&& 逻辑的 AND [[ ${x} -lt 100 && ${y} -gt 100 ]] 返回 false
` `

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
x=10
y=20

echo "x=${x}, y=${y}"

if [[ ${x} -lt 100 && ${y} -gt 100 ]]
then
echo "${x} -lt 100 && ${y} -gt 100 返回 true"
else
echo "${x} -lt 100 && ${y} -gt 100 返回 false"
fi

if [[ ${x} -lt 100 || ${y} -gt 100 ]]
then
echo "${x} -lt 100 || ${y} -gt 100 返回 true"
else
echo "${x} -lt 100 || ${y} -gt 100 返回 false"
fi

# Output:
# x=10, y=20
# 10 -lt 100 && 20 -gt 100 返回 false
# 10 -lt 100 || 20 -gt 100 返回 true

字符串运算符

下表列出了常用的字符串运算符,假定变量 a 为 “abc”,变量 b 为 “efg”:

运算符 说明 举例
= 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
!= 检测两个字符串是否相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 检测字符串长度是否为 0,为 0 返回 true。 [ -z $a ] 返回 false。
-n 检测字符串长度是否为 0,不为 0 返回 true。 [ -n $a ] 返回 true。
str 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。

💻 “示例源码”

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
x="abc"
y="xyz"


echo "x=${x}, y=${y}"

if [[ ${x} = ${y} ]]; then
echo "${x} = ${y} : x 等于 y"
else
echo "${x} = ${y}: x 不等于 y"
fi

if [[ ${x} != ${y} ]]; then
echo "${x} != ${y} : x 不等于 y"
else
echo "${x} != ${y}: x 等于 y"
fi

if [[ -z ${x} ]]; then
echo "-z ${x} : 字符串长度为 0"
else
echo "-z ${x} : 字符串长度不为 0"
fi

if [[ -n "${x}" ]]; then
echo "-n ${x} : 字符串长度不为 0"
else
echo "-n ${x} : 字符串长度为 0"
fi

if [[ ${x} ]]; then
echo "${x} : 字符串不为空"
else
echo "${x} : 字符串为空"
fi

# Output:
# x=abc, y=xyz
# abc = xyz: x 不等于 y
# abc != xyz : x 不等于 y
# -z abc : 字符串长度不为 0
# -n abc : 字符串长度不为 0
# abc : 字符串不为空

文件测试运算符

文件测试运算符用于检测 Unix 文件的各种属性。

属性检测描述如下:

操作符 说明 举例
-b file 检测文件是否是块设备文件,如果是,则返回 true。 [ -b $file ] 返回 false。
-c file 检测文件是否是字符设备文件,如果是,则返回 true。 [ -c $file ] 返回 false。
-d file 检测文件是否是目录,如果是,则返回 true。 [ -d $file ] 返回 false。
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 [ -f $file ] 返回 true。
-g file 检测文件是否设置了 SGID 位,如果是,则返回 true。 [ -g $file ] 返回 false。
-k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 [ -k $file ]返回 false。
-p file 检测文件是否是有名管道,如果是,则返回 true。 [ -p $file ] 返回 false。
-u file 检测文件是否设置了 SUID 位,如果是,则返回 true。 [ -u $file ] 返回 false。
-r file 检测文件是否可读,如果是,则返回 true。 [ -r $file ] 返回 true。
-w file 检测文件是否可写,如果是,则返回 true。 [ -w $file ] 返回 true。
-x file 检测文件是否可执行,如果是,则返回 true。 [ -x $file ] 返回 true。
-s file 检测文件是否为空(文件大小是否大于 0),不为空返回 true。 [ -s $file ] 返回 true。
-e file 检测文件(包括目录)是否存在,如果是,则返回 true。 [ -e $file ] 返回 true。

💻 “示例源码”

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
file="/etc/hosts"

if [[ -r ${file} ]]; then
echo "${file} 文件可读"
else
echo "${file} 文件不可读"
fi
if [[ -w ${file} ]]; then
echo "${file} 文件可写"
else
echo "${file} 文件不可写"
fi
if [[ -x ${file} ]]; then
echo "${file} 文件可执行"
else
echo "${file} 文件不可执行"
fi
if [[ -f ${file} ]]; then
echo "${file} 文件为普通文件"
else
echo "${file} 文件为特殊文件"
fi
if [[ -d ${file} ]]; then
echo "${file} 文件是个目录"
else
echo "${file} 文件不是个目录"
fi
if [[ -s ${file} ]]; then
echo "${file} 文件不为空"
else
echo "${file} 文件为空"
fi
if [[ -e ${file} ]]; then
echo "${file} 文件存在"
else
echo "${file} 文件不存在"
fi

# Output:(根据文件的实际情况,输出结果可能不同)
# /etc/hosts 文件可读
# /etc/hosts 文件可写
# /etc/hosts 文件不可执行
# /etc/hosts 文件为普通文件
# /etc/hosts 文件不是个目录
# /etc/hosts 文件不为空
# /etc/hosts 文件存在

控制语句

条件语句

跟其它程序设计语言一样,Bash 中的条件语句让我们可以决定一个操作是否被执行。结果取决于一个包在[[ ]]里的表达式。

[[ ]]sh中是[ ])包起来的表达式被称作 检测命令基元。这些表达式帮助我们检测一个条件的结果。这里可以找到有关bash 中单双中括号区别的答案。

共有两个不同的条件表达式:ifcase

if

(1)if 语句

if在使用上跟其它语言相同。如果中括号里的表达式为真,那么thenfi之间的代码会被执行。fi标志着条件代码块的结束。

1
2
3
4
5
6
7
8
9
10
# 写成一行
if [[ 1 -eq 1 ]]; then echo "1 -eq 1 result is: true"; fi
# Output: 1 -eq 1 result is: true

# 写成多行
if [[ "abc" -eq "abc" ]]
then
echo ""abc" -eq "abc" result is: true"
fi
# Output: abc -eq abc result is: true

(2)if else 语句

同样,我们可以使用if..else语句,例如:

1
2
3
4
5
6
if [[ 2 -ne 1 ]]; then
echo "true"
else
echo "false"
fi
# Output: true

(3)if elif else 语句

有些时候,if..else不能满足我们的要求。别忘了if..elif..else,使用起来也很方便。

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
x=10
y=20
if [[ ${x} > ${y} ]]; then
echo "${x} > ${y}"
elif [[ ${x} < ${y} ]]; then
echo "${x} < ${y}"
else
echo "${x} = ${y}"
fi
# Output: 10 < 20

case

如果你需要面对很多情况,分别要采取不同的措施,那么使用case会比嵌套的if更有用。使用case来解决复杂的条件判断,看起来像下面这样:

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
exec
case ${oper} in
"+")
val=`expr ${x} + ${y}`
echo "${x} + ${y} = ${val}"
;;
"-")
val=`expr ${x} - ${y}`
echo "${x} - ${y} = ${val}"
;;
"*")
val=`expr ${x} \* ${y}`
echo "${x} * ${y} = ${val}"
;;
"/")
val=`expr ${x} / ${y}`
echo "${x} / ${y} = ${val}"
;;
*)
echo "Unknown oper!"
;;
esac

每种情况都是匹配了某个模式的表达式。|用来分割多个模式,)用来结束一个模式序列。第一个匹配上的模式对应的命令将会被执行。*代表任何不匹配以上给定模式的模式。命令块儿之间要用;;分隔。

循环语句

循环其实不足为奇。跟其它程序设计语言一样,bash 中的循环也是只要控制条件为真就一直迭代执行的代码块。

Bash 中有四种循环:forwhileuntilselect

for循环

for与它在 C 语言中的姊妹非常像。看起来是这样:

1
2
3
4
for arg in elem1 elem2 ... elemN
do
### 语句
done

在每次循环的过程中,arg依次被赋值为从elem1elemN。这些值还可以是通配符或者大括号扩展

当然,我们还可以把for循环写在一行,但这要求do之前要有一个分号,就像下面这样:

1
for i in {1..5}; do echo $i; done

还有,如果你觉得for..in..do对你来说有点奇怪,那么你也可以像 C 语言那样使用for,比如:

1
2
3
for (( i = 0; i < 10; i++ )); do
echo $i
done

当我们想对一个目录下的所有文件做同样的操作时,for就很方便了。举个例子,如果我们想把所有的.bash文件移动到script文件夹中,并给它们可执行权限,我们的脚本可以这样写:

💻 “示例源码”

1
2
3
4
5
DIR=/home/zp
for FILE in ${DIR}/*.sh; do
mv "$FILE" "${DIR}/scripts"
done
# 将 /home/zp 目录下所有 sh 文件拷贝到 /home/zp/scripts

while循环

while循环检测一个条件,只要这个条件为 _真_,就执行一段命令。被检测的条件跟if..then中使用的基元并无二异。因此一个while循环看起来会是这样:

1
2
3
4
while [[ condition ]]
do
### 语句
done

for循环一样,如果我们把do和被检测的条件写到一行,那么必须要在do之前加一个分号。

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
### 0到9之间每个数的平方
x=0
while [[ ${x} -lt 10 ]]; do
echo $((x * x))
x=$((x + 1))
done
# Output:
# 0
# 1
# 4
# 9
# 16
# 25
# 36
# 49
# 64
# 81

until循环

until循环跟while循环正好相反。它跟while一样也需要检测一个测试条件,但不同的是,只要该条件为 就一直执行循环:

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
11
x=0
until [[ ${x} -ge 5 ]]; do
echo ${x}
x=`expr ${x} + 1`
done
# Output:
# 0
# 1
# 2
# 3
# 4

select循环

select循环帮助我们组织一个用户菜单。它的语法几乎跟for循环一致:

1
2
3
4
select answer in elem1 elem2 ... elemN
do
### 语句
done

select会打印elem1..elemN以及它们的序列号到屏幕上,之后会提示用户输入。通常看到的是$?PS3变量)。用户的选择结果会被保存到answer中。如果answer是一个在1..N之间的数字,那么语句会被执行,紧接着会进行下一次迭代 —— 如果不想这样的话我们可以使用break语句。

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env bash

PS3="Choose the package manager: "
select ITEM in bower npm gem pip
do
echo -n "Enter the package name: " && read PACKAGE
case ${ITEM} in
bower) bower install ${PACKAGE} ;;
npm) npm install ${PACKAGE} ;;
gem) gem install ${PACKAGE} ;;
pip) pip install ${PACKAGE} ;;
esac
break # 避免无限循环
done

这个例子,先询问用户他想使用什么包管理器。接着,又询问了想安装什么包,最后执行安装操作。

运行这个脚本,会得到如下输出:

1
2
3
4
5
6
7
$ ./my_script
1) bower
2) npm
3) gem
4) pip
Choose the package manager: 2
Enter the package name: gitbook-cli

breakcontinue

如果想提前结束一个循环或跳过某次循环执行,可以使用 shell 的breakcontinue语句来实现。它们可以在任何循环中使用。

break语句用来提前结束当前循环。

continue语句用来跳过某次迭代。

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
# 查找 10 以内第一个能整除 2 和 3 的正整数
i=1
while [[ ${i} -lt 10 ]]; do
if [[ $((i % 3)) -eq 0 ]] && [[ $((i % 2)) -eq 0 ]]; then
echo ${i}
break;
fi
i=`expr ${i} + 1`
done
# Output: 6

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
11
12
13
# 打印10以内的奇数
for (( i = 0; i < 10; i ++ )); do
if [[ $((i % 2)) -eq 0 ]]; then
continue;
fi
echo ${i}
done
# Output:
# 1
# 3
# 5
# 7
# 9

函数

bash 函数定义语法如下:

1
2
3
4
[ function ] funname [()] {
action;
[return int;]
}

💡 说明:

  1. 函数定义时,function 关键字可有可无。
  2. 函数返回值 - return 返回函数返回值,返回值类型只能为整数(0-255)。如果不加 return 语句,shell 默认将以最后一条命令的运行结果,作为函数返回值。
  3. 函数返回值在调用该函数后通过 $? 来获得。
  4. 所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至 shell 解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。

💻 “示例源码”

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
#!/usr/bin/env bash

calc(){
PS3="choose the oper: "
select oper in + - \* / # 生成操作符选择菜单
do
echo -n "enter first num: " && read x # 读取输入参数
echo -n "enter second num: " && read y # 读取输入参数
exec
case ${oper} in
"+")
return $((${x} + ${y}))
;;
"-")
return $((${x} - ${y}))
;;
"*")
return $((${x} * ${y}))
;;
"/")
return $((${x} / ${y}))
;;
*)
echo "${oper} is not support!"
return 0
;;
esac
break
done
}
calc
echo "the result is: $?" # $? 获取 calc 函数返回值

执行结果:

1
2
3
4
5
6
7
8
9
$ ./function-demo.sh
1) +
2) -
3) *
4) /
choose the oper: 3
enter first num: 10
enter second num: 10
the result is: 100

位置参数

位置参数是在调用一个函数并传给它参数时创建的变量。

位置参数变量表:

变量 描述
$0 脚本名称
$1 … $9 第 1 个到第 9 个参数列表
${10} … ${N} 第 10 个到 N 个参数列表
$* or $@ 除了$0外的所有位置参数
$# 不包括$0在内的位置参数的个数
$FUNCNAME 函数名称(仅在函数内部有值)

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env bash

x=0
if [[ -n $1 ]]; then
echo "第一个参数为:$1"
x=$1
else
echo "第一个参数为空"
fi

y=0
if [[ -n $2 ]]; then
echo "第二个参数为:$2"
y=$2
else
echo "第二个参数为空"
fi

paramsFunction(){
echo "函数第一个入参:$1"
echo "函数第二个入参:$2"
}
paramsFunction ${x} ${y}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
$ ./function-demo2.sh
第一个参数为空
第二个参数为空
函数第一个入参:0
函数第二个入参:0

$ ./function-demo2.sh 10 20
第一个参数为:10
第二个参数为:20
函数第一个入参:10
函数第二个入参:20

执行 ./variable-demo4.sh hello world ,然后在脚本中通过 $1$2 … 读取第 1 个参数、第 2 个参数。。。

函数处理参数

另外,还有几个特殊字符用来处理参数:

参数处理 说明
$# 返回参数个数
$* 返回所有参数
$$ 脚本运行的当前进程 ID 号
$! 后台运行的最后一个进程的 ID 号
$@ 返回所有参数
$- 返回 Shell 使用的当前选项,与 set 命令功能相同。
$? 函数返回值

💻 “示例源码”

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
runner() {
return 0
}

name=zp
paramsFunction(){
echo "函数第一个入参:$1"
echo "函数第二个入参:$2"
echo "传递到脚本的参数个数:$#"
echo "所有参数:"
printf "+ %s\n" "$*"
echo "脚本运行的当前进程 ID 号:$$"
echo "后台运行的最后一个进程的 ID 号:$!"
echo "所有参数:"
printf "+ %s\n" "$@"
echo "Shell 使用的当前选项:$-"
runner
echo "runner 函数的返回值:$?"
}
paramsFunction 1 "abc" "hello, \"zp\""
# Output:
# 函数第一个入参:1
# 函数第二个入参:abc
# 传递到脚本的参数个数:3
# 所有参数:
# + 1 abc hello, "zp"
# 脚本运行的当前进程 ID 号:26400
# 后台运行的最后一个进程的 ID 号:
# 所有参数:
# + 1
# + abc
# + hello, "zp"
# Shell 使用的当前选项:hB
# runner 函数的返回值:0

Shell 扩展

扩展 发生在一行命令被分成一个个的 记号(tokens) 之后。换言之,扩展是一种执行数学运算的机制,还可以用来保存命令的执行结果,等等。

感兴趣的话可以阅读关于 shell 扩展的更多细节

大括号扩展

大括号扩展让生成任意的字符串成为可能。它跟 文件名扩展 很类似,举个例子:

1
echo beg{i,a,u}n ### begin began begun

大括号扩展还可以用来创建一个可被循环迭代的区间。

1
2
echo {0..5} ### 0 1 2 3 4 5
echo {00..8..2} ### 00 02 04 06 08

命令置换

命令置换允许我们对一个命令求值,并将其值置换到另一个命令或者变量赋值表达式中。当一个命令被``或$()包围时,命令置换将会执行。举个例子:

1
2
3
4
5
now=`date +%T`
### or
now=$(date +%T)

echo $now ### 19:08:26

算数扩展

在 bash 中,执行算数运算是非常方便的。算数表达式必须包在$(( ))中。算数扩展的格式为:

1
2
result=$(( ((10 + 5*3) - 7) / 2 ))
echo $result ### 9

在算数表达式中,使用变量无需带上$前缀:

1
2
3
4
5
x=4
y=7
echo $(( x + y )) ### 11
echo $(( ++x + y++ )) ### 12
echo $(( x + y )) ### 13

单引号和双引号

单引号和双引号之间有很重要的区别。在双引号中,变量引用或者命令置换是会被展开的。在单引号中是不会的。举个例子:

1
2
echo "Your home: $HOME" ### Your home: /Users/<username>
echo 'Your home: $HOME' ### Your home: $HOME

当局部变量和环境变量包含空格时,它们在引号中的扩展要格外注意。随便举个例子,假如我们用echo来输出用户的输入:

1
2
3
INPUT="A string  with   strange    whitespace."
echo $INPUT ### A string with strange whitespace.
echo "$INPUT" ### A string with strange whitespace.

调用第一个echo时给了它 5 个单独的参数 —— $INPUT 被分成了单独的词,echo在每个词之间打印了一个空格。第二种情况,调用echo时只给了它一个参数(整个$INPUT 的值,包括其中的空格)。

来看一个更严肃的例子:

1
2
3
FILE="Favorite Things.txt"
cat $FILE ### 尝试输出两个文件: `Favorite` 和 `Things.txt`
cat "$FILE" ### 输出一个文件: `Favorite Things.txt`

尽管这个问题可以通过把 FILE 重命名成Favorite-Things.txt来解决,但是,假如这个值来自某个环境变量,来自一个位置参数,或者来自其它命令(find, cat, 等等)呢。因此,如果输入 可能 包含空格,务必要用引号把表达式包起来。

流和重定向

Bash 有很强大的工具来处理程序之间的协同工作。使用流,我们能将一个程序的输出发送到另一个程序或文件,因此,我们能方便地记录日志或做一些其它我们想做的事。

管道给了我们创建传送带的机会,控制程序的执行成为可能。

学习如何使用这些强大的、高级的工具是非常非常重要的。

输入、输出流

Bash 接收输入,并以字符序列或 字符流 的形式产生输出。这些流能被重定向到文件或另一个流中。

有三个文件描述符:

代码 描述符 描述
0 stdin 标准输入
1 stdout 标准输出
2 stderr 标准错误输出

重定向

重定向让我们可以控制一个命令的输入来自哪里,输出结果到什么地方。这些运算符在控制流的重定向时会被用到:

Operator Description
> 重定向输出
&> 重定向输出和错误输出
&>> 以附加的形式重定向输出和错误输出
< 重定向输入
<< Here 文档 语法
<<< Here 字符串

以下是一些使用重定向的例子:

1
2
3
4
5
6
7
8
9
10
11
### ls的结果将会被写到list.txt中
ls -l > list.txt

### 将输出附加到list.txt中
ls -a >> list.txt

### 所有的错误信息会被写到errors.txt中
grep da * 2> errors.txt

### 从errors.txt中读取输入
less < errors.txt

/dev/null 文件

如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:

1
$ command > /dev/null

/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到”禁止输出”的效果。

如果希望屏蔽 stdout 和 stderr,可以这样写:

1
$ command > /dev/null 2>&1

Debug

shell 提供了用于 debug 脚本的工具。

如果想采用 debug 模式运行某脚本,可以在其 shebang 中使用一个特殊的选项:

1
#!/bin/bash options

options 是一些可以改变 shell 行为的选项。下表是一些可能对你有用的选项:

Short Name Description
-f noglob 禁止文件名展开(globbing)
-i interactive 让脚本以 交互 模式运行
-n noexec 读取命令,但不执行(语法检查)
-t 执行完第一条命令后退出
-v verbose 在执行每条命令前,向stderr输出该命令
-x xtrace 在执行每条命令前,向stderr输出该命令以及该命令的扩展参数

举个例子,如果我们在脚本中指定了-x例如:

1
2
3
4
5
#!/bin/bash -x

for (( i = 0; i < 3; i++ )); do
echo $i
done

这会向stdout打印出变量的值和一些其它有用的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ./my_script
+ (( i = 0 ))
+ (( i < 3 ))
+ echo 0
0
+ (( i++ ))
+ (( i < 3 ))
+ echo 1
1
+ (( i++ ))
+ (( i < 3 ))
+ echo 2
2
+ (( i++ ))
+ (( i < 3 ))

有时我们值需要 debug 脚本的一部分。这种情况下,使用set命令会很方便。这个命令可以启用或禁用选项。使用-启用选项,+禁用选项:

💻 “示例源码”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 开启 debug
set -x
for (( i = 0; i < 3; i++ )); do
printf ${i}
done
# 关闭 debug
set +x
# Output:
# + (( i = 0 ))
# + (( i < 3 ))
# + printf 0
# 0+ (( i++ ))
# + (( i < 3 ))
# + printf 1
# 1+ (( i++ ))
# + (( i < 3 ))
# + printf 2
# 2+ (( i++ ))
# + (( i < 3 ))
# + set +x

for i in {1..5}; do printf ${i}; done
printf "\n"
# Output: 12345

参考资料

最后,Stack Overflow 上 bash 标签下有很多你可以学习的问题,当你遇到问题时,也是一个提问的好地方。

Vim 应用

概念

什么是 vim

Vim 是从 vi 发展出来的一个文本编辑器。代码补完、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。和 Emacs 并列成为类 Unix 系统用户最喜欢的编辑器。

Vim 的模式

基本上 vi/vim 共分为三种模式,分别是命令模式(Command mode)插入模式(Insert mode)底线命令模式(Last line mode)

命令模式

用户刚刚启动 vi/vim,便进入了命令模式。

此状态下敲击键盘动作会被 Vim 识别为命令,而非输入字符。

插入模式

在命令模式下按下 i 就进入了输入模式。

在输入模式下,你可以输入文本内容。

底线命令模式

在命令模式下按下 :(英文冒号)就进入了底线命令模式。

底线命令模式可以输入单个或多个字符的命令,可用的命令非常多。

Vim 渐进学习

存活

  1. 安装 vim
  2. 启动 vim
  3. 什么也别干!请先阅读

当你安装好一个编辑器后,你一定会想在其中输入点什么东西,然后看看这个编辑器是什么样子。但 vim 不是这样的,请按照下面的命令操作:

  • 启 动 Vim 后,vim 在 Normal 模式下。
  • 让我们进入 Insert 模式,请按下键 i 。(注:你会看到 vim 左下角有一个–insert–字样,表示,你可以以插入的方式输入了)
  • 此时,你可以输入文本了,就像你用“记事本”一样。
  • 如果你想返回 Normal 模式,请按 ESC 键。

现在,你知道如何在 InsertNormal 模式下切换了。下面是一些命令,可以让你在 Normal 模式下幸存下来:

  • iInsert 模式,按 ESC 回到 Normal 模式.
  • x → 删当前光标所在的一个字符。
  • :wq → 存盘 + 退出 (:w 存盘, :q 退出) (注::w 后可以跟文件名)
  • dd → 删除当前行,并把删除的行存到剪贴板里
  • p → 粘贴剪贴板

推荐

  • hjkl (强例推荐使用其移动光标,但不必需) → 你也可以使用光标键 (←↓↑→). 注: j 就像下箭头。
  • :help <command> → 显示相关命令的帮助。你也可以就输入 :help 而不跟命令。(注:退出帮助需要输入:q)

你能在 vim 幸存下来只需要上述的那 5 个命令,你就可以编辑文本了,你一定要把这些命令练成一种下意识的状态。于是你就可以开始进阶到第二级了。

当是,在你进入第二级时,需要再说一下 Normal 模式。在一般的编辑器下,当你需要 copy 一段文字的时候,你需要使用 Ctrl 键,比如:Ctrl-C。也就是说,Ctrl 键就好像功能键一样,当你按下了功能键 Ctrl 后,C 就不在是 C 了,而且就是一个命令或是一个快键键了,在 vim 的 Normal 模式下,所有的键都是功能键。这个你需要知道。

标记

  • 下面的文字中,如果是 Ctrl-λ我会写成 <C-λ>.
  • : 开始的命令你需要输入 <enter>回车,例如 — 如果我写成 :q 也就是说你要输入 :q<enter>.

感觉良好

上面的那些命令只能让你存活下来,现在是时候学习一些更多的命令了,下面是我的建议:(注:所有的命令都需要在 Normal 模式下使用,如果你不知道现在在什么样的模式,你就狂按几次 ESC 键)

  1. 各种插入模式

    • a → 在光标后插入
    • o → 在当前行后插入一个新行
    • O → 在当前行前插入一个新行
    • cw → 替换从光标所在位置后到一个单词结尾的字符
  2. 简单的移动光标

    • 0 → 数字零,到行头
    • ^ → 到本行第一个不是 blank 字符的位置(所谓 blank 字符就是空格,tab,换行,回车等)
    • $ → 到本行行尾
    • g_ → 到本行最后一个不是 blank 字符的位置。
    • /pattern → 搜索 pattern 的字符串(注:如果搜索出多个匹配,可按 n 键到下一个)
  3. 拷贝/粘贴

    (注:p/P 都可以,p 是表示在当前位置之后,P 表示在当前位置之前)

    • P → 粘贴
    • yy → 拷贝当前行当行于 ddP
  4. Undo/Redo

    • u → undo
    • <C-r> → redo
  5. 打开/保存/退出/改变文件

    (Buffer)

    • :e <path/to/file> → 打开一个文件
    • :w → 存盘
    • :saveas <path/to/file> → 另存为 <path/to/file>
    • :xZZ:wq → 保存并退出 (:x 表示仅在需要时保存,ZZ 不需要输入冒号并回车)
    • :q! → 退出不保存 :qa! 强行退出所有的正在编辑的文件,就算别的文件有更改。
    • :bn:bp → 你可以同时打开很多文件,使用这两个命令来切换下一个或上一个文件。(注:我喜欢使用:n 到下一个文件)

花点时间熟悉一下上面的命令,一旦你掌握他们了,你就几乎可以干其它编辑器都能干的事了。但是到现在为止,你还是觉得使用 vim 还是有点笨拙,不过没关系,你可以进阶到第三级了。

更好,更强,更快

先恭喜你!你干的很不错。我们可以开始一些更为有趣的事了。在第三级,我们只谈那些和 vi 可以兼容的命令。

更好

下面,让我们看一下 vim 是怎么重复自己的:1515G

  1. . → (小数点) 可以重复上一次的命令
  2. N<command> → 重复某个命令 N 次

下面是一个示例,找开一个文件你可以试试下面的命令:

  • 2dd → 删除 2 行
  • 3p → 粘贴文本 3 次
  • 100idesu [ESC] → 会写下 “desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu desu “
  • . → 重复上一个命令—— 100 “desu “.
  • 3. → 重复 3 次 “desu” (注意:不是 300,你看,VIM 多聪明啊).

更强

你要让你的光标移动更有效率,你一定要了解下面的这些命令,千万别跳过

  1. NG → 到第 N 行 (注:注意命令中的 G 是大写的,另我一般使用 : N 到第 N 行,如 :137 到第 137 行)

  2. gg → 到第一行。(注:相当于 1G,或 :1)

  3. G → 到最后一行。

  4. 按单词移动:

    1. w → 到下一个单词的开头。
    2. e → 到下一个单词的结尾。

    > 如果你认为单词是由默认方式,那么就用小写的 e 和 w。默认上来说,一个单词由字母,数字和下划线组成(注:程序变量)

    > 如果你认为单词是由 blank 字符分隔符,那么你需要使用大写的 E 和 W。(注:程序语句)

    img

下面,让我来说说最强的光标移动:

  • % : 匹配括号移动,包括 (, {, [. (注:你需要把光标先移到括号上)
  • *#: 匹配光标当前所在的单词,移动光标到下一个(或上一个)匹配单词(*是下一个,#是上一个)

相信我,上面这三个命令对程序员来说是相当强大的。

更快

你一定要记住光标的移动,因为很多命令都可以和这些移动光标的命令连动。很多命令都可以如下来干:

<start position><command><end position>

例如 0y$ 命令意味着:

  • 0 → 先到行头
  • y → 从这里开始拷贝
  • $ → 拷贝到本行最后一个字符

你可可以输入 ye,从当前位置拷贝到本单词的最后一个字符。

你也可以输入 y2/foo 来拷贝 2 个 “foo” 之间的字符串。

还有很多时间并不一定你就一定要按 y 才会拷贝,下面的命令也会被拷贝:

  • d (删除 )
  • v (可视化的选择)
  • gU (变大写)
  • gu (变小写)
  • 等等

(注:可视化选择是一个很有意思的命令,你可以先按 v,然后移动光标,你就会看到文本被选择,然后,你可能 d,也可 y,也可以变大写等)

Vim 超能力

你只需要掌握前面的命令,你就可以很舒服的使用 VIM 了。但是,现在,我们向你介绍的是 VIM 杀手级的功能。下面这些功能是我只用 vim 的原因。

在当前行上移动光标: 0 ^ ####fFtT,``;`

  • 0 → 到行头
  • ^ → 到本行的第一个非 blank 字符
  • $ → 到行尾
  • g_ → 到本行最后一个不是 blank 字符的位置。
  • fa → 到下一个为 a 的字符处,你也可以 fs 到下一个为 s 的字符。
  • t, → 到逗号前的第一个字符。逗号可以变成其它字符。
  • 3fa → 在当前行查找第三个出现的 a。
  • FT → 和 ft 一样,只不过是相反方向。
    img

还有一个很有用的命令是 dt" → 删除所有的内容,直到遇到双引号—— "。

区域选择 <action>a<object><action>i<object>

在 visual 模式下,这些命令很强大,其命令格式为

<action>a<object><action>i<object>

  • action 可以是任何的命令,如 d (删除), y (拷贝), v (可以视模式选择)。
  • object 可能是: w 一个单词, W 一个以空格为分隔的单词, s 一个句字, p 一个段落。也可以是一个特别的字符:"、 '、 )、 }、 ]。

假设你有一个字符串 (map (+) ("foo")).而光标键在第一个 o的位置。

  • vi" → 会选择 foo.
  • va" → 会选择 "foo".
  • vi) → 会选择 "foo".
  • va) → 会选择("foo").
  • v2i) → 会选择 map (+) ("foo")
  • v2a) → 会选择 (map (+) ("foo"))

img

块操作: <C-v>

块操作,典型的操作: 0 <C-v> <C-d> I-- [ESC]

  • ^ → 到行头
  • <C-v> → 开始块操作
  • <C-d> → 向下移动 (你也可以使用 hjkl 来移动光标,或是使用%,或是别的)
  • I-- [ESC] → I 是插入,插入“--”,按 ESC 键来为每一行生效。

img

在 Windows 下的 vim,你需要使用 <C-q> 而不是 <C-v><C-v> 是拷贝剪贴板。

自动提示: <C-n><C-p>

在 Insert 模式下,你可以输入一个词的开头,然后按 <C-p>或是<C-n>,自动补齐功能就出现了……

img

宏录制: qa 操作序列 q, @a, @@

  • qa 把你的操作记录在寄存器 a。
  • 于是 @a 会 replay 被录制的宏。
  • @@ 是一个快捷键用来 replay 最新录制的宏。

示例

在一个只有一行且这一行只有“1”的文本中,键入如下命令:

  • qaYp<C-a>q
    

    • qa 开始录制
    • Yp 复制行.
    • <C-a> 增加 1.
    • q 停止录制.
  • @a → 在 1 下面写下 2

  • @@ → 在 2 正面写下 3

  • 现在做 100@@ 会创建新的 100 行,并把数据增加到 103.

img

可视化选择: v,V,<C-v>

前面,我们看到了 <C-v>的示例 (在 Windows 下应该是 <C-q>),我们可以使用 vV。一但被选好了,你可以做下面的事:

  • J → 把所有的行连接起来(变成一行)
  • <> → 左右缩进
  • = → 自动给缩进 (注:这个功能相当强大,我太喜欢了)

img

在所有被选择的行后加上点东西:

  • <C-v>
  • 选中相关的行 (可使用 j<C-d> 或是 /pattern 或是 % 等……)
  • $ 到行最后
  • A, 输入字符串,按 ESC。

img

分屏: :splitvsplit.

下面是主要的命令,你可以使用 VIM 的帮助 :help split. 你可以参考本站以前的一篇文章VIM 分屏

  • :split → 创建分屏 (:vsplit创建垂直分屏)
  • <C-w><dir> : dir 就是方向,可以是 hjkl 或是 ←↓↑→ 中的一个,其用来切换分屏。
  • <C-w>_ (或 <C-w>|) : 最大化尺寸 (<C-w>| 垂直分屏)
  • <C-w>+ (或 <C-w>-) : 增加尺寸

img

Vim Cheat Sheet

本节内容的原文地址:http://cenalulu.github.io/linux/all-vim-cheatsheat/

经典版

下面这个键位图应该是大家最常看见的经典版了。其实这个版本是一系列的入门教程键位图的组合结果。要查看不同编辑模式下的键位图,可以看这里打包下载

此外,这里还有简体中文版。

img

入门版

基本操作的入门版。原版出处还有 keynote 版本可供 DIY 以及其他相关有用的 cheatsheet。

img

进阶版

下图是 300DPI 的超清大图,另外查看原文还有更多版本:黑白,低分辨率,色盲等

img

增强版

下图是一个更新时间较新的现代版,含有的信息也更丰富。原文链接

img

文字版

原文链接

img

img

资料