shell 语法

Table of Contents

1 帮助

任何 shell 书籍或者文档都不可能覆盖命令的所有参数使用,在 Linux 中,当需要更多命令帮助时,请使用 man 和 info。

man 是简短的帮助文档,GNU 觉得 man 比较落后,开发了 info,info 具备超链接,以及一些开发文档,内容更加丰富,多数时候通过 man 只能获得大致信息,更详细的可以用 info 查看,所以 info 可和 man 起互补作用。pinfo 是具备 lynx 浏览器风格的 info。

1.1 man

通常会看到一些文档上提示使用man参考帮助信息,格式如:ls(1)。

括号里的数字叫“章节编号”,例如 stat(1) 和 stat(2) 虽然名字一样,但代表了不同的帮助,具体的章节号信息,通过 man man 可以找到。

1.1.1 搜索

-k:按关键字搜索帮助,如:

$ man -k search

whatis 命令也搜索手册的概述,可以用通配符、正则。

1.1.2 更新 man 库

man 中的文档是由 mandb 命令更新。在 crontab 下有一个自动更新的脚本,默认系统会每天自动更新:

/etc/cron.daily/man-db.cron

如果库更新失败,在用 man -k 搜索时就会出错:

$ man -k passwd
passwd: nothing appropriate.

这种情况直接手动运行 mandb 命令更新即可。

1.2 其他命令

help:Bash 内置的命令,语法帮助,注意 zsh 中没有该命令。

2 快捷键

我们会有很大一部分时间是在 shell 中生活,掌握了快捷键可以大大提高效率,下面我总结了常用的快捷键,掌握全部则大大提高你的工作效率。

移动:

Alt+b 前移一个单词
Alt+f 后移一个单词
Ctrl+e(End) 移动到行尾
Ctrl+b 往左移动一个字符
Ctrl+f 往右移动一个字符
Esc+b 往左移动一个单词
Esc+f 往右移动一个单词

删除字符:

Del或Ctrl+d 删除光标所在位置的字符
Back Space(Ctrl+h) 删除光标所在字符的前一个字符
Esc+d 删除一个单词
Ctrl+退格、Ctrl+w 删除前一个单词
Alt+d 删除后面单词,删除单词后,命令保存到内存中的,使用 Ctrl+y 粘贴
Alt+y 循环粘贴

删除行:

Ctrl+k 由光标开始,删除右边至行尾的所有字符
Ctrl+u 由光标开始,删除左边至行首的所有字符
Ctrl+a、Ctrl+k 删除整行

复原操作:

Ctrl+y 把之前删除的字符或字符串复制到光标所在位置(Alt+y 可以循环粘贴)

重复执行:

Esc+N(重复次数) 重复操作 n 次
Alt+. 上一条命令的最后参数
Alt+<n> 上一条命令第 n 个参数,这里的“参数”和命令接受的“参数”不一样,这里的参数是上条命令按空格分割开的。比如要使用上条命令第二个参数:Alt+4、Alt+.

搜索历史:

Ctrl+r 搜索执行过的命令
Ctrl+p 调出前一个命令
Ctrl+n 调出下一条命令
Esc+< 调出第一个历史命令
Esc+> 移到最后个历史命令后面,即等待键入指令的现行命令行

补全:

Tab 或 Esc+.(或 _ )  
[email protected] 补全主机名
Esc+~ 补全用户名
Esc+$ 补全变量名

其他:

Ctrl+l 清屏
Ctrl+s 冻结当前输入,Ctrl+q 解冻

3 shell 脚本

“#!”出现在 Linux/Unix 脚本第一行,称作为“Shebang”。源于发音符号“#”(读作Sharp)和“!”(读作Bang),合起来就是Sharp-bang,“Shebang”是“Sharp-bang”的缩写。

4 通配符过滤

*:匹配全部 ?:代表一个字符

5 子 shell

coproc shell 提供的协程功能,让命令在新生成的子 shell 中执行。

“&” jobs fg

6 环境变量和 shell 变量

在 shell 中赋值的变量叫作 shell 变量,变量只对当前 shell 有效;需要让所有通过当前 shell 运行的进程都可以访问的变量,就是环境变量,环境变量一般用于对某些程序做一些“个性设置”,把信息存在内存中,方便程序和脚本读取。

定义变量的语法如下:

var=value

定义字符串变量:

msg=hello

如果要用空格:

msg=hello\ world

更好的方式是用引号包围:

msg='hello world'

如果要在单引号字符串中引用单引号字符,不能用“\'”这样的转义,正确写法是“'\''”:

$ echo 'I'\''m lu4nx'
I'm lu4nx

shell 会把空格当作命令参数,所以定义变量的“=”之间是不能有空格的,如果你写成:

var = value

shell 会把 var 当作一个命令,“=”和“value”是它的参数。在 shell 中需要警惕空格。

引用变量:

${var}或$var

例,分割字符串:

分割字符串:
$  s="hello world"
$  echo ${s:0:5}
hello

通过 export 关键字,可以定义一个环境变量。

打印环境变量:

printenv env

6.1 IFS

可以使用 IFS 变量设定分割符,例如:

IFS=':'

然后调用:

read $r1 $r2 $r3 $r4 $r5 $r6 $r7 < /etc/passwd

因为 passwd 用了“:”将内容分割成了 7 段,所以这样就可以将 7 段的值分别存到了 7 个变量中了。

6.2 EDITOR,默认编辑器设置

export $EDITOR=vim

使用 Ctrl+x+e 组合快捷键可以打开默认编辑器(对 zsh 无效)。

6.3 重要的环境变量

$PS1    命令提示符,通常是$字符,但在 bash 中,你可以使用一些更复杂的值。例如,字符串 [\[email protected]\h \W]$ 就是一个流行的默认值,它给出用户名、机器名和当前目录名,当然也包括一个$提示符
$PS2    二级提示符,用来提示后续的输入,通常是>字符
$IFS        输入域分隔符。当 shell 读取输入时,它给出用来分隔单词的一组字符,它们通常是空格、制表符和换行符
$0      shell 脚本的名字,等同与 SHELL 变量,名字取决于运行方式:./xx.sh、/path/xx.sh 和 bash xx.sh,一般配合 basename 使用
$#      传递给脚本的参数个数,字符串中引用:${!#}
$$      shell 脚本的进程号,脚本程序通常会用它来生成一个唯一的临时文件,如 /tmp/tmpfile_$$
$!      上一个被执行的命令的 PID(后台运行的进程)
$?      上一个命令的退出状态(管道命令使用 ${PIPESTATUS})
$UID 当前用户的 UID
$_ 上一条命令的最后一个参数,放在脚本顶端,可以取到当前脚本的路径(因为这时最后一条命令就是shell执行脚本的命令,最后一个参数自然是文件路径

7 shell 初始化

几个相关文件:

/etc/profile,不要去修改,可能升级系统时被覆盖掉。如果需要所有用户共享,把脚本放在 /etc/profile.d下最好
~/.bash_profile
~/.bash_login
~/.profile

几个术语:

  • 登录 shell:登录shell时输入帐号和密码,这是登录 shell
  • 非登录 shell:用 bash 命令运行 shell 脚本
  • 交互式 shell:就是交互式输入命令

区别在于,“登录shell”会尝试读取:/etc/profile、~/.bashprofile、~/.bashlogin和~/.profile。

8 标准输入/输出/错误

shell 最多允许用 9 个句柄,其中 0~2 已分配给预留的 3 个文件描述符:

0:标准输入,stdin 1:标准输出,stdout 2:标准错误,stderr

标准输出和标准错误都用于输出消息,之所以要分开区分,是为了在重定向时过滤掉无用的消息,比如一些警告消息可以输出到标准错误,而不影响到标准输出。

<:重定向输入 >:重定向输出,新建或覆盖 command 2>test:重定向标准错误 >>:重定向追加 &>:标准输出、标准错误一同输出 >&1、>&2:输出内容统一到标准输出和标准错误 command 2>&1 或 command &>,标准错误重定向到标准输出 exec 3>&-:&-表示关闭文件句柄

将多条命令的输出重定向到一个文件中:

cat<<EOF > output.txt
$(
cat /etc/passwd
echo ================================
cat /etc/hosts
)
EOF

8.1 tee

可以将内容同时输出到两个地方,从标准输入读入数据并输出到标准输出以及文件中。

例,读入数据并写入到其他文件中:

cat /etc/hosts | tee /tmp/hosts.bak

例,把输出的内容用sudo追加到文件中:

echo -n "127.0.0.1\twww.baidu.com" | sudo tee -a /etc/hosts

要用echo xxx > /etc/xxx 这种需要 root 权限的来说,得这样:

echo xxx | sudo tee --append /etc/xx

8.2 read

交互式输入:

read name
echo $name

9 一切为字符串

shell 对所有输入都当作字符串,也就是 shell 是没有整数、浮点数这些数据类型的,更无法直接做运算,若是直接像其他编程语言一样做四则表达式:

$ 1+1
bash: 1+1: 未找到命令
$ 1 + 1
bash: 1: 未找到命令

第一条语句,shell 把它当作一个命令来执行;第二条语句有了空格分割,shell 把“1”当作了命令,“+”和“1”都当作了参数。

如果要做运算,需要借助外力:

  • 方法1,expr 命令(不能用浮点数):
expr 1 + 1 # => 2
  • 方法2,Bash 内置的 let 命令:
$ let a=1+1
$ echo $a
2
  • 方法3,bc 命令,比较推荐,因为它支持复杂的运算和浮点数
$ echo 5.1 + 1 | bc
6.1
  • 方法4,$[…]
$ echo $[ 1 + 3 ]
4
  • 方法5,$((…))
$ echo $((1+1))
2

双括号还支持很多丰富的运算:

val++
val--
++val
--val
!
~
**
<<
>>
&
|
&&
||

9.1 Here Document

COMMAND <<EOF
...
...
...
EOF

如果要输出到文件:

COMMAND <<EOF > outfile
….
….
….
EOF

10 数组

数组在日常的 shell 编程中用得不太多。

array=(item1 item2 item3)
# 索引数组
echo ${array[0]}
# 更新数组元素
array[0]=xx
# 遍历
for item in ${array[@]}
do
    echo $item
done

取数组长度:\({变量名[*]}或\){变量名[@]}

${test[*]}:获取整个数组内容

array=(item1 item2 item3)
echo ${#array}

# 也可以取字符串长度
msg='hello world'
echo ${#msg}

用 unset 可以删除某些值。

11 子程序

# 两种定义函数的方式
function hello {
    echo hello world
}

hello1() {
    echo hello world
}

11.1 全局变量和局部变量

函数中定义的变量,默认是全区作用域:

function test() {
    a=1024
    echo $a
}

test # => 1024
echo $a # => 1024

如果不小心和全局变量重名了,会导致变量被莫名其妙修改。加 local 关键字可以定义局部变量:

function test1() {
    n=2048
    echo $n
}

test # => 2048
echo $n # 无法输出

11.2 子程序参数

$n:n 为第几个参数,参数从 $1、$2 … 中取,超过 9 的参数,需要通过 ${…} 引用。

function say {
    echo "you say: $1"
}

$* 和 [email protected]:将参数当作单个参数。区别:

count=0
for arg in "$*"
do
    count=$[ $count + 1 ]
done
echo $count # => 1

count1=0
for arg in "[email protected]"
do
    count1=$[ $count1 + 1 ]
done
echo $count1 # => 3

shift 负责跳过参数:

while [ -n "$1" ]
do
    echo $1
    shift
done

输出如下:

./test.sh 1 2 3 4
1
2
3
4

11.3 传递数组参数

function hello {
    echo $@
}
myarray=(a b c d)
hello ${myarray[*]}

11.4 返回数组

function hello {
    local myarray=(a b c d)
    echo ${myarray[*]}
}

newarray=$(hello)
echo ${newarray[*]}

11.5 参数变量

$N 脚本程序的参数($1, $2, $3 …) $* 在一个变量中列出所有的参数,各个参数之间用环境变量 IFS 中的第一个字符分隔开。如果 IFS 被修改了,那么 $* 将命令行分割为参数的方式就将随之改变 [email protected] 它是 $* 的一种精巧的变体,它不使用 IFS 环境变量,所以即使 IFS 为空,参数也不会挤在一起

12 条件判断

if [ 表达式 ]; then
    ...
elif [ 表达式 ]; then
    ...
else
    ...
fi

if 只能根据命令返回状态码做判断。

表达式组合:

[ expr ] # 表达式为真
[ ! expr ] # 表达式不为真
! [ expr ] # 同上
[ expr1 -a expr2 ] # 类似:if(expr1 && expr2)
[ expr1 -o expr2 ] # 类似:if(expr1 || expr2)
# 复合测试
[ ... ] && [ ... ]
[ ... ] || [ ... ]

12.1 [ 和 test 的区别

“[ .. ]”之间是有空格分开的,不能紧凑地写在一起,因为“[”实际上是一个系统命令:

$ which [
/usr/bin/[

“[”之后的都是命令的参数,所以需要空格分割。为了统一,需要用“]”作为最后一个参数。

“[”其实和 test 命令是等价的,记不住表达式的话,可以:man test。

12.2 条件表达式

字符串比较 string1 = string2 如果两个字符串相同则结果为真 string1 != string2 如果两个字符串不同则结果为真 -n string 如果字符串不为空则结果为真 -z string 如果字符串为null(一个空串)则结果为真

算术比较 expression1 -eq expression2 如果两个表达式相等则结果为真 expression1 -ne expression2 如果两个表达式不等则结果为真 expression1 -gt expression2 如果expression1大于expression2则结果为真 expression1 -ge expression2 如果expression1大于等于expression2则结果为真 expression1 -lt expression2 如果expression1小于expression2则结果为真 expression1 -le expression2 如果expression1小于等于expression2则结果为真 ! expression 如果表达式为假则结果为真,反之亦然

文件条件测试 -d file 如果文件是一个目录则结果为真 -e file 如果文件存在则结果为真。要注意的是,历史上-e选项不可移植,所以通常使用的是-f选项 -f file 如果文件是一个普通文件则结果为真 -g file 如果文件的set-group-id位被设置则结果为真 -r file 如果文件可读则结果为真 -s file 如果文件的大小不为0则结果为真 -u file 如果文件的set-user-id位被设置则结果为真 -w file 如果文件可写则结果为真 -x file 如果文件可执行则结果为真

12.3 case-in-esac

case $1 in
    msg)
        echo $2;;
    *)
        echo default
esac

12.4 双方括号

[[ ... ]]

提供模式匹配,示例:

reg='systemd-private-[0-9a-zA-Z]+-([a-zA-Z]+)\.service-[0-9a-zA-Z]+'
for f in /tmp/*
do
    if [[ $f =~ $reg ]]; then
        # 显示后台服务的进程名
        echo ${BASH_REMATCH[1]}
    fi
done

13 循环

13.1 for循环

语法:

for var in list
do
    commands
done

示例:

for i in 1 2 3; do echo $i; done

for结合ls命令的应用:

for file in $(ls);do echo $file; done

但是,有些环境中,ls命令默认给别名化时带了-F参数,如果这样列文件的话,会有问题:

$ for file in $(ls -F);do echo $file; done
deploy/
html/
Makefile
Makefile.el
src/
static/
templates/
tool/

输出的内容包含了表示文件类型的字符。列文件最好的方式:

for file in *; do echo $file; done

for循环的C语言风格版:

for (( i=1; i<=10; i++))
do
    echo $i
done

其实“list”参数只是特定分割符的分开的元素,默认的分割符号有空格、Tab和换行符号,下面的命令是等价的:

for i in $(echo -n "1\t2\t3"); do echo $i; done

for i in $(echo -n "1\n2\n3"); do echo $i; done

有时候想控制分割符,直接修改IFS即可:

str='a,b,c'
IFS=$','
for i in $str
do
    echo $i
done

13.2 while和until

语法:

while test command
do
    other commands
done

until和while类似,但相反。

13.3 循环控制

break和continue。

14 模块化

把shell代码拆分到不同的文件,用source或者“.”加载其他文件。

15 脚本化

查找参数的三种常见方式。

第一种,缺点是无法处理多个参数:

if ! [ -n "$1" ]
then
    echo no args
    exit
fi

case $1 in
    start)
        echo starting...;;
    stop)
        echo stoping...;;
    status)
        echo ok;;
    *)
        echo "I don't known"
esac

第二种,使用getopt:

set -- $(getopt -q ab:cd "[email protected]") # -- 表示参数结束

while [ -n "$1" ]
do
    case "$1" in
        -a) echo "Found the -a option" ;;
        -b) param="$2"
            echo "Found the -b option, with parameter value $param"
            shift ;;
        -c) echo "Found the -c option" ;;
        --) shift
            break ;;
        *) echo "$1 is not an option";;
    esac
    shift
done
#
count=1
for param in "[email protected]"
do
    echo "Parameter #$count: $param"
    count=$[ $count + 1 ]
done

第三种,使用比getopt更高级的getopts:

while getopts :ab:c opt
do
    case "$opt" in
        a) echo "Found the -a option" ;;
        b) echo "Found the -b option, with value $OPTARG";;
        c) echo "Found the -c option" ;;
        *) echo "Unknown option: $opt";;
    esac
done