第2章 入门

2.1 脚本语言 vs 编译型语言

类型 代表语言 特点
编译型 C/C++、Java、Pascal 源码编译成机器码;执行速度快;偏向底层字节、数字处理,做文件操作麻烦
脚本解释型 Shell、awk、Python、Perl 由解释器逐行读取执行;开发快,擅长文件目录;性能偏弱,大部分场景够用

shell优势:POSIX标准化,各大UNIX/Linux都自带,一次编写,多处运行。三大优点:简单、可移植、开发快。

2.2 第一个简单脚本

需求:统计当前登录用户数量
交互式命令:

1
who | wc -l

开发流程:先命令行测试 → 写入文件 → 增加执行权限 → 运行脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 1写入文件
cat > nusers
who | wc -l
^D   # Ctrl+D结束输入

# 2增加执行权限
chmod +x nusers

# 3运行
./nusers

2.3 #! 幻数行(脚本第一行)

内核读取脚本文件开头两个字符#!,后面是解释器完整路径,告诉内核用哪个程序执行本脚本。

示例:

1
2
#!/bin/sh -
who | wc -l
  • #!/bin/sh:指定使用sh解释器执行脚本
  • 末尾的-安全用途,防御欺骗攻击。

其他解释器例子:

1
2
#!/bin/csh -f
#!/bin/awk -f

⚠注意坑点

  1. #!这一行有字符长度限制,尽量控制在64字符以内。
  2. 必须写解释器完整绝对路径;不同系统路径不一样。
  3. 老系统内核不识别#!,shell会自动降级调用/bin/sh
  4. POSIX标准没有定义#!,属于系统扩展功能。

2.4 Shell基础元素

2.4.1 命令、选项、参数

命令行格式:命令 选项 参数

  • 选项以-开头;短选项可以合并:ls -lt等价ls -l -t
  • --:双横杠,代表选项结束,后面全部当做文件名参数。
  • ;:同一行多条命令分隔,顺序执行。
  • &:放到命令末尾,放到后台执行,不等命令结束继续往下跑。

shell三类命令

  1. 内建命令:shell程序本身实现,不创建新进程。cd read echo printf test
  2. shell函数:shell语言编写,像命令一样调用,第6章讲解。
  3. 外部命令:磁盘上可执行程序。执行流程:
    1. fork创建子进程
    2. $PATH目录列表搜索命令;如果命令带/(比如./nusers)跳过PATH查找
    3. 子进程替换为目标程序执行
    4. 程序结束,回到父shell。

2.4.2 Shell变量

变量:给一段字符串起名字;shell变量全部保存字符串,可以是空。

规则:

  • 变量名:字母/下划线开头,后面字母数字下划线
  • 赋值:变量=值等号两边绝对不能有空格!
  • 取值:$变量名;复杂场景 ${变量名}
  • 变量值包含空格,必须使用双引号包裹。

示例

1
2
3
4
5
6
7
8
myvar="hello world"
echo $myvar

first=isaac
middle=bashevis
last=singer
fullname="$first $middle $last"   # 拼接,需要引号
oldname=$fullname

2.4.3 echo输出(可移植性差)

不同系统echo行为不一样,‑n、转义字符行为实现各不相同。简单输出可以用;复杂输出优先选printf

转义序列 含义
\a 蜂鸣告警
\b 退格
\c 停止输出,不输出换行
\n 换行
\t 制表tab
1
echo hello world

2.4.4 printf输出(推荐,POSIX标准,跨平台)

语法:

1
printf "格式模板" 参数1 参数2 ...

重点:printf不会自动输出换行!需要手动写\n

  • %s 字符串占位
  • %d 数字占位 模板用完参数还没处理完,会循环复用模板。

示例

1
printf "first: %s, second: %s\n" hello world
  1. 格式串里的占位符%s %d按顺序,依次消耗后面的参数
  2. ⚠特殊规则:参数比占位符多的时候,会循环复用格式字符串! 这个是 shell printf 非常容易踩坑的点。

2.5 I/O重定向与管道

三个标准流:
标准输入stdin、标准输出stdout、标准错误stderr;默认绑定终端。
过滤器:读取标准输入,输出标准输出的程序。

重定向符号

符号 作用
< file 把程序标准输入改为来自file文件
> file 标准输出重定向到file;文件存在直接覆盖!
>> file 标准输出追加到file末尾,不覆盖原有内容
cmd1 cmd2 管道:cmd1 标准输出 → cmd2 标准输入

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 删除文件回车符,输出新文件
tr -d '\r' < dos-file.txt > unix-file.txt

# 循环追加
for f in dos-file*.txt
do
  tr -d '\r' < "$f" >> big-file.txt
done

# 管道,who输出交给wc -l
who | wc -l

管道技巧:尽量把过滤数据的操作放管道前面,减少后续程序处理的数据量。例如先grep筛选,再sort排序。

两个特殊文件

  1. /dev/null 位桶:写入的数据全部丢弃;读取立刻返回EOF。
1
2
3
4
5
6
# 只关心grep是否匹配,不需要输出,丢到/dev/null

if grep pattern myfile > /dev/null
then
  echo "找到匹配"
fi
  1. /dev/tty:永远指向当前终端。适合输入密码场景,即使标准输入被重定向,仍然从键盘读取。
1
2
3
4
5

printf "Enter new password: "
stty -echo          # 关闭回显,输入密码看不见
read pass < /dev/tty
stty echo           # 恢复回显

tr命令速查

1
2

tr [选项] 源字符集 替换字符集
选项 说明
-c 取反(字节层面)
-C 取反(字符locale层面,部分系统不支持)
-d 删除字符,不做替换
-s 压缩连续重复字符为1个

tr只处理标准输入输出,不接收文件名参数,必须配合<重定向。

2.6 PATH命令查找路径

$PATH:冒号分隔目录列表,shell在这里搜索外部命令。

1
2
3

echo $PATH
/bin:/usr/bin:/usr/local/bin

实践:建立个人$HOME/bin存放自己写的脚本

1
2
3
4
5
6

cd ~
mkdir bin
mv nusers ~/bin
PATH="$PATH:$HOME/bin"   # 将个人bin追加到PATH末尾
nusers

永久生效写进 ~/.profile
⚠安全提示:不要把当前目录.放在PATH,有安全风险。

2.7 位置参数(脚本命令行参数)

脚本执行 ./finduser betsybetsy 就是第一个参数。

  • $1 第1个参数,$2第2个
  • 参数大于9,必须大括号 ${10}

示例脚本 finduser

1
2
3
4
5
6

# !/bin/sh

# finduser: 查询某个用户是否登录

who | grep "$1"

运行:./finduser betsy

如果不给参数,$1为空,grep直接报错。第6章学习判断参数是否存在。

2.8 脚本调试:执行跟踪 set -x

  • set -x:打开跟踪,执行每条命令前打印这条命令,前面带+
  • set +x:关闭跟踪

两种使用方式

  1. 命令行调试脚本:
1
2

sh -x nusers
  1. 写在脚本内部:
1
2
3
4
5
6

# !/bin/sh
set -x
echo 第一句
set +x
echo 第二句

2.9 locale 国际化本地化

locale影响排序、字符集、时间、货币格式。写可移植脚本经常设置LC_ALL=C强制ASCII传统行为。

环境变量优先级:LC_ALL > 各个LC_* > LANG

变量 用途
LANG 总默认设置
LC_ALL 最高优先级,全部覆盖,脚本常用LC_ALL=C sort
LC_COLLATE 排序规则(sort受它影响)
LC_CTYPE 字符集,字母、标点识别
LC_MESSAGES 程序提示报错消息语言
LC_TIME 时间日期格式

查看本机支持哪些locale:

1
2

locale -a

C / POSIX locale:传统ASCII模式,写shell脚本推荐用,行为稳定,不受系统语言干扰。

本章速查表

语法/命令 简单说明 示例
#!/bin/sh - 脚本开头幻数行 #!/bin/sh -
var=value 变量赋值,等号无空格 name="hello shell"
$var ${var} 读取变量 echo "$name"
printf "a %s\n" x 格式化输出,必须手动\n printf "num:%d\n" 10
cmd < file 标准输入重定向 tr -d '\r' < in.txt
cmd > file 输出覆盖文件 who > user.txt
cmd >> file 输出追加文件 echo hello >> log.txt
cmd1 cmd2 管道,前输出作为后输入 who | wc -l
/dev/null 丢弃输出 grep xxx f >/dev/null
/dev/tty 强制终端读写 read pass </dev/tty
$PATH 命令搜索目录 PATH=$PATH:$HOME/bin
$1 ${10} 脚本位置参数 ./script arg1 arg2
set -x / set +x 打开关闭脚本调试跟踪 set -x; command; set +x
LC_ALL=C 强制C locale,脚本稳定 LC_ALL=C sort file.txt

小结

  1. 变量赋值=两边不能空格
  2. echo各个系统行为不一致,复杂输出优先printf,记得加\n换行。
  3. > file会直接覆盖原有文件,注意不要误删数据;追加用>>
  4. 管道尽量先过滤再排序,减少数据量,提升效率。
  5. 脚本参数大于9,用 ${10},不能写$10
  6. 排序、正则字符范围受locale影响,可移植脚本加上LC_ALL=C
  7. 外部命令带斜杠./xxx不会走PATH搜索。