Linux 系统编程

第二章 补充与进阶

2.1 Linux 常用命令

补充:根目录结构

目录名称 内容
/bin 用户可执行文件,如ps ls ping
/sbin 系统管理所用的可执行文件
/etc 所有程序需要的配置文件
/dev 设备文件
/proc 虚拟的文件系统,包含系统进程的相关信息
/var 变量文件,如系统日志、数据库文件等
/tmp 系统和用户创建的临时文件
/usr 用户程序
/home 用户个人文档
/boot 引导加载程序的相关文件
/lib 系统库
/opt 可选的附加应用程序
/mnt 临时安装目录,用于挂载文件系统
/media 可移动媒体设备目录,用于挂载 CD-ROM 等设备的临时目录
/srv 服务器特定服务的数据

一、文件系统相关命令

列出内容
1
2
3
ls         # 列出当前目录的内容  
ls -a /etc # 列出 /etc 所有内容(包括隐藏文件)
ls -l ~ # 列出 ~ 内容的详细信息
查看当前目录
1
pwd
创建目录
1
2
mkdir bin              # 在当前目录下创建子目录 bin
mkdir -p /tmp/me/test # 递归创建目录
切换工作目录
1
2
3
cd /usr/local # 切换到指定路径
cd .. # 切换到父目录
cd - # 切换到上一个所在的目录
删除空目录
1
rmdir /tmp/mytmp # 删除空白目录
拷贝
1
2
3
cp /etc/passwd ~/Desktop     # 将文件 /etc/passwd 复制到 ~/Desktop 下
cp -r /tmp/test /root # 将目录 /tmp/test 复制到 /root 下
cp -rp /tmp/t1 /tmp/t2 /root # 将目录 /tmp/t1 和 /tmp/t2 复制到 /root 下,保留原有属性
删除
1
2
3
rm a.c             # 删除当前目录下的 a.c
rm -f /tmp/log.txt # 强制删除
rm -rf /tmp/test # 强制删除目录

二、文件处理命令

创建文件
1
2
touch a.cpp
touch /tmp/errlog.txt
查看文件内容
1
2
cat /etc/passwd
cat -n /etc/passwd # 显示行号
分屏查看文件
1
2
more a.txt
less a.txt
取首尾 n 行内容
1
2
head -n 10 /etc/passwd
tail -n 15 /etc/passwd
统计文本
1
2
3
4
5
wc /etc/passwd    # 输出行数、单词数、字节数、文件名
wc -l /etc/passwd # 统计行数
wc -w /etc/passwd # 统计单词数
wc -m /etc/passwd # 统计字符数
wc -c /etc/passwd # 统计字节数
创建链接
1
2
ln hello.c hello.hl    # 创建硬链接
ln -s hello.c hello.sl # 创建软链接
创建命名管道
1
mkfifo s.pipe

三、权限管理命令

修改权限
1
2
3
4
5
chmod 421 testfile   # 赋予文件 testfile 所有者读,所属组写,其他用户执行的权限
chmod -R 660 testdir # 递归地赋予目录 testdir 所有者、所属组读写的权限
chmod u+r testfile # 为文件 testfile 的所有者添加读权限
chmod g+w testfile # 为文件 testfile 的所属组添加写权限
chmod o+x testfile # 为其他用户添加对 testfile 的执行权限

补充:读写执行权限对于文件和目录的含义

权限 对文件的含义 对目录的含义
读权限 可以查看文件内容 可以列出目录内容
写权限 可以修改文件内容 可以在其中创建文件
可执行权限 可以执行文件 可以进入目录
修改文件所有者
1
chown xiaoxingou test1 /tmp/test2 # 将两个文件的所有者改成 xiaoxingou
修改文件所属组
1
chgrp group1 text1 /tmp/text2 # 将两个文件的所属组改为 gruop1

四、搜索相关命令

查找文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# find [搜索范围] [匹配条件]
find /etc -name init # 查找名为 init 的文件
find /etc -name init\* # 查找名字以 init 开头的文件
find /etc -name init??? # 查找名字为 init 加上 3 个字符的文件

find /etc -size +100M # 查找大小超过 100M 的文件
find /etc -size 60K # 查找大小为 60K 的文件
find /etc -size -5G # 查找大小小于 5G 的文件

find ~ -user abc # 查找属于用户 abc 的文件
find ~ -group abc # 查找属于组 abc 的文件

find /etc -atime -5s # 查找访问时间小于 5s 的文件
find /etc -atime -20m # 查找访问实现小于 20min 的文件
find /etc -ctime +6h # 查找修改权限时间超过 6h 的文件
find /etc -mtime +10d # 查找修改内容时间超过 10 天的文件
find /etc -mtime -4w # 查找修改内容时间小于 4 周的文件

find ~ -name test\* -a -type f # 查找名字以 test 开头且类型为普通文件的文件
find ~ -type d -o -type l # 查找类型为目录或软链接的文件
查看命令所在目录及别名信息
1
which ls
查看命令所在位置及帮助文档路径
1
whereis ls
文本搜索(过滤)
1
2
3
4
5
6
7
8
9
10
11
12
grep root a.txt          # 查找包含 "root" 的行
grep -c root a.txt # 统计包含 "root" 的总行数
grep -i RoOt a.txt # 不区分大小写查找
grep -n root a.txt # 查找包含 "root" 的行并打印行号行号
grep -v root a.txt # 查找不包含 "root" 的行
grep -E "123.[45]" a.txt # 查找包含 {"123." 并紧跟 "4" 或 "5"} 的行 ** -E 表示启用正则表达式 **
grep -E -v "^123" a.txt # 查找行首不是 "123" 的行
grep -E "^[Rr]oot" a.txt # 查找以 "Root" 或 "root" 开头的行
grep -E "r..t" a.txt # 查找包含 {"r" 紧跟任意两字符随后紧跟 "t"} 的行
grep -E "w\{2,\}" a.txt # 查找 "w" 连续出现两次或以上的行
grep -E "w\{3,5\}" a.txt # 查找 "w" 连续出现三次或五次的行
grep -n -E "^$" a.txt # 查找空行并输出行号

五、帮助命令

查看文档
1
2
3
4
man man        # 查看 man 命令文档
man ls # 查看 ls 命令文档
man 2 write # 在编号为 2 的系统调用手册里查看 write 函数文档
man 3 sem_init # 在编号为 3 的 C 库函数手册里查看 sen_init 函数文档
获取简介
1
whatis ls
查看详细信息
1
info ls

六、用户管理命令

添加用户
1
useradd username
修改密码
1
passwd username
查看登录用户信息
1
who
查看登录用户详细信息
1
w
查看历史登录信息
1
last
切换用户
1
2
3
4
5
6
su            # 切换到 root 用户
su root # 切换到 root 用户
su - # 切换到 root 用户并把目录切换到 /root
su - root # 切换到 root 用户并把目录切换到 /root
su somebody # 切换到普通用户 somebody
su - somebody # 切换到普通用户 somebody 并把目录切换到其所在目录

七、压缩与解压缩命令

压缩
1
2
3
4
gzip testfile # 压缩 testfile 生成 testfile.gz(不能压缩目录)

zip testfile.zip testfile # 压缩文件
zip -r testdir.zip testdir # 压缩目录
解压缩
1
2
3
gunzip test.gz

unzip test.zip
归档
1
2
tar -zcf testdir testdir.tar.gz  # -c 打包,-z 打包并压缩,-f 指定文件名
tar -zxvf testdir.tar.gz testdir # -x 解包,-z 解压缩,-v 输出详细信息,-f 指定文件名

八、网络相关命令

发送信息
1
write xiaoxingou # 给用户 xiaoxingou 发送信息,以 control + D 保存结束
广播
1
wall Hello # 给所有用户发送信息 Hello
测试网络连通性
1
2
ping baidu.com      # 持续测试
ping -c 5 baidu.com # 测试五次
查看和设置网卡信息
1
ifconfig
查看网络信息
1
2
3
4
5
6
7
8
9
netstat
# -t TCP 协议
# -u UDP 协议
# -l 监听
# -r 路由
# -n 显示 IP 地址和端口号
netstat -ltun # 查看本机监听的端口
netstat -an # 查看本机所有的网络连接
netstat -rn # 查看本机的路由表

九、系统相关命令

打印当前时间
1
date
打印日历
1
2
3
cal         # 打印本月日历
cal 10 2003 # 打印 2003 年 10 月的日历
cal 2020 # 打印 2020 年的日历
关机
1
2
3
shutdown -h now   # 立即关机
shutdown -h +10 # 10 分钟后关机
shutdonw -h 23:30 # 特定时间关机
立即关机
1
poweroff
立即重启
1
reboot

2.2 vim 编辑器

一、vim 的四种模式及其切换

截屏2023-01-12 16.43.33

二、普通模式

光标移动
输入 操作
h 或 [退格] 或 [左箭头] 左移一个字符
j 或 [下箭头] 下移一行
k 或 [上箭头] 上移一行
l 或 [空格] 或 [右箭头] 右移一个字符
w 移到下一个单词开头
W 移到下一个单词开头,忽略一些标点
b 移到上一个单词开头
B 移到上一个单词开头,忽略一些标点
+ 或 [Enter] 移到下一行第一个非空白字符处
- 移到上一行第一个非空白字符处
fc 移到同一行下一个字符 c 处
Fc 移到同一行上一个字符 c 处
tc 移到同一行下一个字符 c 前
Tc 移到同一行上一个字符 c 前
; 配合 f 或 t 使用,重复一次
, 配合 f 或 t 使用,反向重复一次

上述输入均可配合数字使用,先输入一个数字 n,再输入该内容,表示重复操作 n 次。

输入 操作
0 或 ^ 移到行首
$ 移到行末
gg 移到文件头
G 移到文件尾部
H 移到屏幕最顶端一行
M 移到屏幕中间一行
L 移到屏幕最底端一行
翻屏
输入 操作
control + f 下翻一屏
control + b 上翻一屏
control +d 下翻半屏
control + u 上翻半屏
control + e 向下滚动一行
control + y 向上滚动一行
n% 滚动到文件 n% 的位置
zz 将当前行移动到屏幕中央
zt 将当前行移动到屏幕顶端
zb 将当前行移动到屏幕底端
文本快速操作
复制
输入 操作
y 复制可视化模式下选中的内容
yy / nyy / yny 复制当前行 / 复制 n 行
y^ 复制当前到行首的内容
y$ 复制当前到行末的内容
yh / ynh / nyh 复制左边字符 / 复制左边 n 个字符
yl / nyl / ynl 复制右边字符 / 复制右边 n 个字符
yw / nyw / ynw 复制当前单词 / 复制 n 个单词
yG 复制到文件尾部
剪切(删除)
输入 操作
d 剪切可视化模式下选中的内容
dd / ndd / dnd 剪切当前行 / 剪切 n 行
d^ 剪切当前到行首的内容
d$ 剪切当前到行末的内容
x / dl / nx / ndl / dnl 剪切右边字符 / 剪切右边 n 个字符
X / dh / nX / ndh / dnh 剪切左边字符 / 剪切左边 n 个字符
dw / ndw / dnw 剪切当前单词 / 剪切 n 个单词
dG 剪切到文件尾部
粘贴
输入 操作
p 粘贴至光标后
P 整行的复制粘贴到上一行 / 非整行的复制粘贴到光标前
撤销与重做
输入 操作
u 撤销
U 撤销当前行最近所有修改
control + r 重做

二、命令模式

查找
命令 操作
:/something 在光标后面的文本中查找 something
:?something 在光标前面的文本中查找 something
n 查找下一个
N 查找上一个
:nohl 关闭高亮
替换
命令 操作
:s/old/new[/i] 用 new 替换当前行第一个 old [忽略大小写]
:s/old/new/g[i][c] 用 new 替换当前行所有 old [忽略大小写] [每次替换前询问]
:n1, n2s/old/new/g[i][c] 用 new 替换 n1 行到 n2 行所有 old [忽略大小写] [每次替换前询问]
:%s/old/new/g[i][c] 用 new 替换所有 old [忽略大小写] [每次替换前询问]
:n1, n2s/^/xxx/g 在 n1 行到 n2 行的行首插入 xxx
:%s/^/xxx/g 在每一行行首插入 xxx
:n1, n2s/$/xxx/g 在 n1 到 n2 行行末插入 xxx
:%s/$/xxx/g 在每一行行末插入 xxx

三、可视化模式

在普通模式下

  • 输入 v 进入字符可视化模式(逐字符选取)

    截屏2023-01-12 21.12.58

  • 输入 V 进入行可视化模式(逐行选取)

    截屏2023-01-12 21.13.25

  • 输入 control + V 进入块可视化模式(选取内容为矩形块)

    截屏2023-01-12 21.13.58

在可视化模式下,光标移过的地方就会被选中,选中后,有类似于普通模式的快捷操作。操作完毕返回普通模式。

输入 操作
y 复制选中内容
Y 复制选中行,哪怕某行没有被完全选中
d 剪切选中内容
D 剪切选中行,哪怕某行没有被完全选中
c 删除选中文本并进入插入模式
C 删除选中行并进入编辑模式,哪怕某行没有被完全选中
> 增加缩进
< 减少缩进
~ 大小写转换
:write filename 将选中内容写入文件

2.3 gcc 编译器

基本的编译选项
1
gcc hello.c    # 编译 hello.c 默认生成 a.out 可执行文件
1
gcc hello.c -o hello    # 编译 hello.c 生成 hello 可执行文件
1
gcc -E hello.c -o hello.i    # 预处理 hello.c 生成 hello.i 文件
1
gcc -S hello.c -o hello.s    # 编译 hello.c 生成 hello.s 汇编代码文件
1
gcc -c hello.c -o hello.o    # 汇编 hello.c 生成 hello.o 可重定位目标文件
1
gcc hello.o -o hello    # 由 hello.o 可重定位目标文件链接成 hello 可执行文件
1
gcc hello.c -o hello -save-temps    # 保留中间文件
1
gcc -o test test.c myfunc.o -Iinclude # 指定头文件路径为当前目录下的 include 目录

由编译过程中的一个或若干个、一种或若干种中间文件,可以生成-o选项后的目标文件,只要该文件处于编译阶段的更下游。

静态库

1. 编写静态库源代码和测试程序
1
2
3
4
5
// ./include/foo.h
#ifndef _FOO_H_
#define _FOO_H_
extern void foo();
#endif
1
2
3
4
// ./src/foo.c
#include "foo.h"
#include <stdio.h>
void foo() { printf("foo\n"); }
1
2
3
4
// ./src/main.c
#include <stdio.h>
#include "foo.h"
int main() { printf("Hello World!\n"); foo(); return 0; }
2. 编译并生成静态库
1
gcc -c bin/foo.c -Iinclude -o obj/foo.o
1
ar rcs lib/libfoo.a obj/foo.o
3. 链接静态库
1
2
# 方法一:编译测试程序,并链接静态库
gcc src/main.c -static lib/libfoo.o -o bin/main
1
2
# 方法二:使用 `-L` 指定库的搜索路径 使用 `-l` 指定库的名称
gcc src/main.c -static -Llib -lfoo -o bin/main
动态库
1. 编写动态库源代码和测试程序
1
2
3
4
5
// ./include/bar.h
#ifndef _BAR_H_
#define _BAR_H_
extern void bar();
#endif
1
2
3
4
// ./src/bar.c
#include "bar.h"
#include <stdio.h>
void bar() { printf("bar\n"); }
1
2
3
4
// ./src/main.c
#include <stdio.h>
#include "bar.h"
int main() { printf("Hello World!\n"); bar(); return 0; }
2. 编译并生成动态库
1
gcc src/bar.c -shared -fPIC -o lib/libbar.so    # -fPIC 选项用于生成位置无关代码
3. 指明动态库的位置
1
2
# 方法一:设置环境变量
export LD_LIBRARY_PATH=$(pwd)/lib
1
2
# 方法二:使用 rpath 将共享库位置嵌入到程序
gcc src/main.c -Llib -lbar -Wl,-rpath=`pwd`/lib -o bin/main
1
2
# 方法三:将动态库添加到系统路径
sudo cp lib/libbar.so /usr/bin
4. 链接动态库
1
2
# 方法一
gcc src/main.c lib/libbar.so -o bin/main
1
2
# 方法二同上,指明库的路径和名称
# 注意,系统的库,如线程库 pthread 等不需要指明路径
补充:静态库与动态库的区别
  • 静态库封装了函数,链接时会把对应的代码嵌入到可执行文件中。静态库的编译速度较快,但每个与静态库链接的程序,都有库中一部分函数代码的拷贝,且在运行时每一份拷贝都要占用相应的物理内存。此外,库的版本升级,就意味着所有用到该库的程序都需要重新链接,非常麻烦。
  • 动态库在链接时并不拷贝代码,而是在目标文件中记录一条引用信息,在程序运行时才根据该信息由动态链接器去定位对应代码的位置并执行,使得可执行程序的体积明显减小。其次,每个动态库在物理内存中只有一份副本,调用它的程序会在各自的虚存中映射同一份副本,节省了内存空间。此外,动态库便于库的版本更新。

2.4 make 工具

make 工具是维护程序项目的便捷手段,它通过一个文本文件去描述项目文件之间的依赖性,通过执行make命令在项目内容修改后进行必要的重编译。此处仅举一简例说明。

项目结构

截屏2023-01-14 11.32.14

源代码
1
2
3
4
// main.c
#include "fun1.h"
#include "fun2.h"
int main() { fun1(); fun2(); return 0; }
1
2
3
4
5
// fun1.h
#ifndef _FUN1_H_
#define _FUN1_H_
extern void fun1();
#endif
1
2
3
4
// fun1.c
#include "fun1.h"
#include <stdio.h>
void fun1() { printf("fun1\n"); }
1
2
3
4
5
// fun2.h
#ifndef _FUN2_H_
#define _FUN2_H_
extern void fun2();
#endif
1
2
3
4
// fun2.c
#include "fun2.h"
#include <stdio.h>
void fun2() { printf("fun2\n"); }
编写 Makefile,指明文件之间的依赖性

一般地,Makefile 中的通用格式为

1
2
3
[目标文件]: [依赖文件 1] [依赖文件 2] ... # 空格分开
[由依赖文件产生目标文件的命令] # 如 gcc [依赖文件 1] [依赖文件 2] -o [目标文件]
# ^ 此处为 tab 而非四个空格

make按照 GNUmakefile、makefile、Makefile 的顺序查找文件。如果描述依赖性的文件为其他名字,应用-f参数指出:make -f othername

本例中的 Makefile 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GCC_FLAGS=-c -Wall # 定义常量,gcc 参数为生成可重定位目标文件及显示一切警告

main: main.o fun1.o fun2.o # 最终目标依赖三个 .o 文件
gcc -Wall main.o fun1.o fun2.o -o main
# 以下为三个 .o 文件各自的生成规则
main.o: src/main.c
gcc $(GCC_FLAGS) src/main.c -Iinclude -o main.o
fun1.o: src/fun1.c
gcc $(GCC_FLAGS) src/fun1.c -Iinclude -o fun1.o
fun2.o: src/fun2.c
gcc $(GCC_FLAGS) src/fun2.c -Iinclude -o fun2.o

.PHONY: clean # PHONY 声明 clean 是一个伪目标,此处用于删除 .o 中间文件
clean:
rm -rf *.o
执行make
  • 执行make,编译构建项目
  • 执行make clean,删除中间文件。由于已经声明 clean 是一个伪目标,make将不去理会目录中有无名为 clean 的文件,也不考虑是否需要更新或生成该文件,而是直接执行其后的rm命令。

2.5 Shell 编程

(一)Shell 概述

一、Shell 的两重含义
  • Shell 是一个命令行解释器,它为用户提供了一个向 Linux 内核发送请求以便运行程序的界面系统级程序。用户可以用 Shell 来启动、运行、挂起、终止、编写程序。
  • Shell 是一种编程语言,是解释执行的脚本语言
二、Shell 的分类
  • Bourne Shell:Unix 从 1979 年开始使用 Bourne Shell,其文件后缀为 .sh。

  • C Shell:主要在 BSD 版的 Unix 系统中使用,其语法与 C 语言类似而得名。

  • Shell 的两种主要语法类型为 Bourne 和 C,互不兼容。前者包括 sh、ksh、bash、psh、zsh 等;后者包括 csh 和 tcsh 等。

    bash 与 sh 兼容,现行 Linux 默认使用 bash 作为 Shell。

三、Shell 脚本的执行方式
  • 编写脚本,如vim test.sh。脚本内容可以理解为一条条命令的集合。
  • 修改权限,如chmod 777 test.sh
  • 执行脚本,如./test.shsh test.sh

(二)echo与格式化输入输出

一、echo
打印字符串,自动换行
1
2
3
4
# 以下命令等价
echo hello
echo "hello"
echo 'hello'
启用或关闭转义
1
2
echo -e "\aabc\bd\"\$\c"   # 发出响铃声,打印 abd"$,\c 转义为不换行
echo -E "\aa\\bc\bd\"\$\c" # 打印 \aa\bc\b"$\c 并换行,注意 \\ \" \$ 等转义字符仍保留
取消自动换行
1
echo -n "hello"
打印变量的值
1
2
3
4
5
6
7
8
9
#! /bin/bash # 此句说明使用的 Shell 是 bash
# 打印系统定义的环境变量的值
echo $SHELL
echo $PATH
# 打印自定义变量的值
name=Tom # 自定义变量,等号左右不能有空格
echo $name # 直接打印,$ 为取值符号
echo "My name is $name." # 在字符串中引用,只能用双引号
echo "\$name = $name" # 打印 $ 符号必须转义
打印命令执行结果
1
2
3
4
5
#! /bin/bash

echo `ls` # 反引号中为命令
echo `pwd`" is the working directory." # 字符串拼接,直接跟在后面,不能有空格
echo `cat /etc/passwd | grep root` # 也可以执行更复杂的命令
二、printf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#! /bin/bash

printf "Hello World\n" # 打印字符串(默认不换行)
printf `cat /etc/passwd | grep root` # 打印命令结果 如果遇到空字符就会停止
echo # 只有一个 echo 表示换行
printf `pwd`" is the working directory.\n" # 拼接字符串

ID=10000
name=Tom
height=175.4
printf $ID # 打印变量
echo # 换行
printf "ID: $ID, name: $name, height: $height\n"
# 在双引号中引用变量
printf "*ID: %-10d, name: %-10s, height: %-10.2f*\n" $ID $name $height
# 格式化输出

(三)Bash 常见功能

一、历史命令与命令补全
  • history命令用来查看历史命令
    • -c选项可以清空历史命令
    • -w选项可以将缓存中的命令写入历史命令保存文件
  • 调用历史命令
    • 上下箭头可以调用历史命令
    • !n可以查看第 n 条历史命令
    • !!可以查看上一条命令
    • !串可以查看最后一条以该串开头的历史命令
  • 补全:输入命令或文件名后按 Tab 可以补全
二、命令别名
  • 设置或查看别名

    1
    2
    3
    alias list=ls # 设置 ls 的别名为 list
    alias # 查看系统中所有的命令别名
    # 注意:需要把配置写入 /root/.bashrc 中才能让别名永久生效
  • 命令执行顺序

    • 第一顺位:用绝对路径或相对路径表示的命令
    • 第二顺位:用别名表示的命令
    • 第三顺位:bash 的内部命令
    • 第四顺位:按照$PATH环境变量定义的目录,顺序查找到的第一个命令
三、重定向
标准输出重定向
命令 功能
命令 > 文件 以覆盖方式将命令正确输出写入文件
命令 >> 文件 以追加方式将命令正确输出写入文件
标准错误重定向
命令 功能
错误命令 2> 文件 以覆盖方式将命令错误输出写入文件
错误命令 2>> 文件 以追加方式将命令错误输出写入文件
同时重定向正误输出
命令 功能
命令 > 文件 2>&1 以覆盖方式同时将正确输出和错误输出写入文件
命令 >> 文件 2>&1 以追加方式同时将正确输出和错误输出写入文件
命令 &> 文件 以覆盖方式同时将正确输出和错误输出写入文件
命令 &>> 文件 以追加方式同时将正确输出和错误输出写入文件
命令 > 文件1 2>> 文件2 以覆盖方式将正确输出写入文件1,以追加方式将错误输出写入文件2
四、多命令执行
格式 功能
命令1; 命令2 顺序执行命令,命令间无关联
命令1 && 命令2 当命令1执行正确,命令2才会执行
命令1 || 命令2 当命令1执行错误,命令2才会执行

(四)变量

一、变量及其分类
1. 环境变量
1
2
3
4
5
6
7
8
echo $PATH     # 环境中的路径
echo $HOME # 用户家目录
echo $SHELL # 当前 Shell 类型
echo $USER # 当前用户名
echo $PWD # 当前路径
echo $TERM # 当前终端类型
echo $HOSTNAME # 当前主机名
# ...
2. 系统变量

此类变量属于系统预定义变量,只能用$引用,不能修改。

名称 含义
$0 当前脚本的名称
$1 $2 ... $9 ${10} ... 当前脚本的第 n 个参数
$* 当前脚本的所有参数(从 1 开始)
$# 当前脚本的参数个数(不包括脚本名称)
$? 命令或程序执行完的状态
$$$$ 程序本身的 PID 号
$! 运行的最后一个进程的 PID 号
3. 用户自定义变量
  • 定义变量 变量名=值
  • 撤销变量 unset 变量名
  • 定义常量 readonly 变量名=值。此类变量不能修改值,也不能unset
  • 查看当前 Shell 中的所有变量 set命令

定义变量的规则

  • 变量名可以由字母、数字和下划线组成,但不能以数字开头

  • 等号两侧不能有空格

  • 变量名一般习惯为大写

  • 将命令的输出结果赋值给变量的两种方式

    1
    2
    A=`date`
    A=$(date)

用户自定义环境变量

命令 export 变量名=变量值,此种做法只能临时设置,重启终端后就会失效。

为系统引入自定义环境变量

  • 在 /etc/profile 中增加一行export 变量名=变量值
  • 保存退出,source /etc/profile
二、算数与逻辑运算
1. 运算符

​ 支持四则运算、取模运算、自增自减、逻辑运算、比较运算、移位运算、位运算、赋值运算。特别,支持单目的+ -运算和幂运算**

2. 整数运算
  • (())运算命令,在前面加上$获取结果

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

    A=$((1 + 2)) # 普通算数运算 A=3
    B=$((4 ** 2)) # 幂运算 B=16
    C=$((--A)) # A 自减为 2,并赋值给 C
    D=$(($A + $B + $C)) # 在表达式中引用变量 前方可以加上 $
    E=$((A * B * C)) # 在表达式中引用变量 也可以直接写变量名
    echo D=$D # D=20
    echo E=$E # E=64
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #! /bin/bash

    A=5
    B=10
    echo $((A < B)) # 输出布尔值 1
    echo $((A >= B)) # 输出布尔值 0
    echo $((A != B)) # 输出布尔值 1

    echo $(($((A ** 3)) > $((B << 3)))) # 5 的 3 次方大于 10 左移 3 位,输出布尔值 1
    1
    2
    3
    4
    5
    6
    7
    8
    #! /bin/bash

    A=100
    B=200
    echo $((A - B, A * 2, B - A * 5)) # 逗号分隔的运算,取最后一个表达式的结果,输出 -300

    MIN=$((A < B ? A : B)) # 三目运算,同 C 语言
    echo MIN=$MIN # 输出 MIN=100
  • let运算命令

    1
    2
    3
    4
    5
    6
    7
    8
    #! /bin/bash

    A=1
    B=2
    let "A += A + 1" # C: A += A + 1; => A = 3
    let "B <<= 2" # C: B <<= 2; => B = 8
    let "C = (A + B) * 3 -2" # C: int C = (A + B) * 3 - 2; => C = 31
    echo $C
3. expr表达式
1
2
3
4
5
6
7
8
9
10
11
#! /bin/bash

A=2
B=3
echo `expr \( $A \* 3 \) '*' \( $B + 1 \)`
# 括号、乘号、尖括号等必须转义,或用''包围,不支持 **
# 算式中任意元素左右必须有空格
# 相当于 echo $(((A * 3) * (B + 1))),输出 24

C=`expr \( $A << 2 \) % $B` # 相当于 C=$(((A << 2) % B))
echo "C = $C" # 输出 2
4. 浮点运算的 bc 工具

bc 是 Linux 内置的计算器,支持浮点运算。在终端执行命令bc,即可进入该计算器;输入quit然后回车即可退出。对于浮点运算,其精度由名为scale的变量控制,默认是 0。改变其值,即可改变小数位数。

截屏2023-01-14 18.36.40

在脚本中的使用

  • 运算较简单时

    1
    2
    3
    4
    5
    6
    #! /bin/bash

    readonly PI=3.14159
    R=2
    S=`echo "scale=3; $PI * $R * $R" | bc` # 通过管道直接交给 bc 运算
    printf "S = %.3f\n" $S
  • 运算较复杂时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #! /bin/bash

    A=14.5
    B=5
    C=32.4

    D=`bc << EOF # 追加 EOF 表示一次运算的开始
    scale = 4 # 设置精度
    a = $A / $B # 定义 bc 内部变量(小写字母) 该变量外部不可使用
    b = $C / $B
    a + b
    EOF
    `
    echo $D

(五)控制流

常见的条件判断

判断字符串是否相等

运算符 含义
= 相等
!= 不相等

判断整数的关系

运算符 含义
-lt 小于
-le 小于等于
-eq 相等
-gt 大于
-ge 大于等于
-ne 不等于

判断文件权限

运算符 含义
-r 有读权限
-w 有写权限
-x 有执行权限

判断文件类型

运算符 含义
-e 文件存在
-f 文件存在且为普通文件
-d 文件存在且为目录
一、条件判断语句if
  • 格式 1

    1
    2
    3
    if [ condition ]; then
    # do something
    fi
  • 格式 2

    1
    2
    3
    4
    5
    if [ condition ]; then 
    # do something
    else
    # do something
    fi
  • 格式 3

    1
    2
    3
    4
    5
    6
    7
    if [ condition1 ]; then
    # do something
    elif [ condition2 ]; then
    # do something
    else
    # do something
    fi

例 测试常见的条件判断

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
#! /bin/bash

if [ $# -ne 5 ]; then # 注意 [ 的右边和 ] 的左边一定要有空格
echo "usage: $0 num str1 str2 file1 file2" # 通过命令行传入比较的对象
exit 1
fi # 注意结束标志不能少
# 测试条件为空的情况
if [ ]; then
echo "no condition is true"
else
echo "no condition is false"
fi
# 测试整数的比较
if [ 80 -lt $1 -a $1 -le 100 ]; then # 多条件写法 1: -a(且) 或 -o(或) 连接
echo "80 < \$1 <= 100"
elif [ 60 -lt $1 ] && [ $1 -le 80 ]; then # 多条件写法 2: 分开写 用 && 或 || 连接
echo "60 < \$1 <= 80"
else # else 后面没有 then !!
echo "\$1 <= 60"
fi
# 测试字符串的比较
if [ $2 = $3 ]; then # 判断字符串是否相等
echo "$2 = $3"
else
echo "$2 != $3"
fi
# 测试文件权限
if [ -x $4 ]; then # 判断文件是否有可执行权限
echo "$4 is executable"
else
echo "$4 is not executable"
fi
# 测试文件类型与存在性
if [ -e $5 ]; then # 判断文件是否存在
echo "file $5 exists"
else
echo "file $5 does not exist"
fi
二、条件分支语句case
  • 格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    case $name in
    "val 1")
    # do something
    ;;
    "val 2")
    # do something
    ;;
    "val 3")
    # do something
    ;;
    *)
    # default
    ;;
    esac

case语句的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#! /bin/bash

if [ $# -ne 1 ]; then
echo "usage: $0 num" # 通过命令行传入一个数 判断星期几
exit 1
fi

case $1 in
"1") # 为简便起见 都看成字符串的比较
echo "Can't get up to work..."
;; # 注意每个分支后的两个分号不能省
"4")
echo "Crazy Thursday!!"
;;
"5")
echo "Finally comes Weekends!!"
;;
*) # 默认情况执行的内容
echo "Nothing to say"
;; # 此处也有分号
esac # 注意最后的结束标志
三、循环语句for
  • 格式 1

    1
    2
    3
    for item in list/array; do
    # do something
    done
  • 格式 2

    1
    2
    3
    for ((初始化循环变量; 循环条件; 修改循环变量)); do
    # do something
    done

例 两类for循环的使用

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
#! /bin/bash

# 案例一:考察系统变量 $* 和 $@ 的区别
# 在普通情况下 它们都是命令行参数的数组
# 在被双引号包裹的情况下 前者为字符串 后者为数组

if [ $# -eq 0 ]; then # 要求命令行传入参数用于遍历
echo "usage: $0 arg1 arg2 ..."
exit 1
fi

i=1
for e in $*; do # 第一种情况:不加引号的 $* 将分行打印
echo "$i: $e"
i=$((i + 1))
done
i=1
for e in $@; do # 第二种情况:不加引号的 $@ 将分行打印
echo "$i: $e"
i=$((i + 1))
done
i=1
for e in "$*"; do # 第三种情况:加了引号的 $* 将作为单个字符串打印
echo "$i: $e"
i=$((i + 1))
done
i=1
for e in "$@"; do # 第四种情况:加了引号的 $@ 仍为一数组
echo "$i: $e"
i=$((i + 1))
done

# 案例二:利用 for 循环的第二种格式进行求和

SUM=0
for ((i=1; i <= 100; i++)); do
SUM=$((SUM + i))
done
echo "SUM = $SUM"

对于案例一,可以如下解释

1
2
3
for e in "arg1 arg2 arg3" "arg4" "arg5 arg6" "arg7"; do
echo $e
done

对于默认情况下都是数组的$*$@

  • 前者加上引号后即成为内含有空格的单个字符串,放在for循环中相当于列表中只有一个元素
  • 后者仍为一个有多个元素的数组
四、循环语句while
  • 格式

    1
    2
    3
    while [ condition ]; do
    # do something
    done

例 计算三位数的水仙花数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#! /bin/bash

i=100
while [ $i -lt 1000 ]; do
ii=$i # 拷贝原数
i1=$((ii % 10)) # 求个位数字
((ii /= 10))
i2=$((ii % 10)) # 求十位数字
((ii /= 10)) # 此时 ii 即百位数字
sum=$((i1 ** 3 + i2 ** 3 + ii ** 3))
if [ $sum -eq $i ]; then
echo $i
fi
((i++))
done
五、循环语句until
  • 格式

    1
    2
    3
    until [ condition ]; do   # 只到满足条件才退出循环
    # do something
    done

例 计算阶乘(不考虑大数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#! /bin/bash

if [ $# -ne 1 ]; then # 命令行传入一个数 n
echo "usage: $0 num"
exit 1
fi

i=1
ANS=1
until [ $i -gt $1 ]; do # 在 i > n 之前都继续循环
ANS=$((ANS * i))
i=$((i + 1))
done
echo "ANS = $ANS"
breakcontinue

这两条控制语句用法同 C。此外,可以在其后跟上一个数字 n,表示连续跳出 n 重循环或从 n 重之外的循环继续执行。