Shell 编程 Shell 介绍 Shell 原意是 “外壳”,跟 kernel(内核)相对应,比喻内核外面的一层,即用户跟内核交互的对话界面。
首先,Shell 是一个程序,提供一个与用户对话的环境。这个环境只有一个命令提示符,让用户从键盘输入命令,所以又称为命令行环境(command line interface,CLI)。Shell 接收到用户输入的命令,将命令送入操作系统执行,并将结果返回给用户。
其次,Shell 是一个命令解释器,解释用户输入的命令。它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 命令写出各种小程序,又称为脚本(script)。这些脚本都通过 Shell 的解释执行,而不通过编译。
终端模拟器:terminal emulator,一个模拟命令行窗口的程序,让用户在一个窗口中使用命令行环境,并且提供各种附加功能,比如调整颜色、字体大小、行距等。
不同 Linux 发行版(准确地说是不同的桌面环境)带有的终端程序是不一样的,比如 KDE 桌面环境的终端程序是 konsole,Gnome 桌面环境的终端程序是 gnome-terminal,用户也可以安装第三方的终端程序。
主要的 Shell 有 sh、bash、csh、tcsh、ksh、zsh、fish;Bash 是目前最常用的 Shell。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 cat /etc/shells echo $SHELL ps chsh -s /bin/zsh sudo chsh -s /usr/bin/zsh rootbash exit command [ arg1 ... [ argN ]]-v --verbose
Shell 终端快捷键
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Tab Ctrl + C Ctrl + D Crtl + A Crtl + E Alt + B Ctrl + ← Alt + F Ctrl + → Crtl + W Alt + D Crtl + R Crtl + G Crtl + ↓ Crtl + L Ctrl + Z fg %n bg %n
注:Shell 脚本中,缩进的标准并没有一个严格的规定,常见的缩进宽度是 2 个或 4 个空格(个人现采用 2 个空格的缩进宽度)
参考资料
Bash 命令报错时,仍会继续执行后面的代码
工具
1 2 3 4 5 6 7 8 9 10 11 12 13 export GOPROXY=https://goproxy.cn,directgo install mvdan.cc/sh/v3/cmd/shfmt@latest shfmt script.sh shfmt -w script.sh -i n -mn -ln
1 2 3 4 5 shellcheck [option] script.sh -s -f
1 npm i -g bash-language-server
语法 运行脚本
脚本第一行以 #!
字符(称为 Shebang)开头,指定解释器;#!/bin/bash
可写为 #!/usr/bin/env bash
1 2 3 4 5 6 bash script.sh chmod +x script.sh ./script.sh
注释 1 2 3 4 5 6 : ' 多行注释 comment 1 comment 2 '
打印输出
变量
定义变量:变量名和等号之间不能有空格
使用变量:在变量名前面加美元符号 $
;可在变量名外面添加花括号,帮助解释器识别变量边界
删除变量:unset
输出变量:export
1 2 3 4 var="letter" echo $var unset var export var=value
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 $0 $n $# $? $_ $* $@ $$ $! $- mkdir directory && cd $_ echo "script name: $0 " echo "arg length: $# " echo "arg1: $1 " echo "arg2: $2 " echo "arg3: $3 " for arg in "$*" ; do echo '$* meaning:' "$arg " done for arg in "$@ " ; do echo '$@ meaning:' "$arg " done
shift
命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数,使得后面的参数向前一位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 env printenv PATH echo $PATH export PATH=$PATH :$HOME /bin export PATH=$HOME /bin:$PATH HOME HOST PATH RANDOM [RANDOM%num] PWD PS1 DISPLAY IFS
引号
单引号不展开任何内容,全部原样输出
双引号会展开变量和命令,特殊字符保留(美元符号、反引号和反斜杠,星号会变成普通字符);保存原始命令的输出格式
1 2 3 4 5 echo $'it\' s' # 单引号中使用单引号,在最前面加 $ echo "it' s" # 在双引号之中使用单引号 echo $(cal) # 单行输出 echo " $(cal)" # 原始格式输出
字符串操作 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 echo ${#str} echo ${#str[0]} ${str:offset:length} echo ${str:1:4} echo ${str:1} echo ${str: -4} echo ${str: -4:2} echo "Hello World" | tr '[:upper:]' '[:lower:]' echo "HELLO WORLD" | awk '{ print tolower($0) }' echo "hello world" | tr '[:lower:]' '[:upper:]' echo "hello world" | awk '{ print toupper($0) }' ${str,,} ${str,} ${str^^} ${str^}
大括号 {}
处理字符串:
主要利用 Bash 的参数展开(parameter expansion)功能来实现
参考:Bash笔记
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var="sample.bk.tar.gz" ${var#*.} ${var##*.} ${var%.*} ${var%%.*} ${var:N:M} ${var/a/b} ${var//a/b} echo beg{i,a,u}n echo {0..5} echo {00..8..2} cp /path/{file1,file2,file3,file4} .
算术运算
简单数学运算:原生 Bash 不支持,可通过 expr
命令实现
1 2 3 val=`expr 2 + 2` val=`expr 2 \* 3`
算术扩展:(())
只能计算整数;会自动忽略内部的空格;支持常用运算符;指出逻辑运算符;支持赋值运算
++
和 --
这两个运算符有前缀和后缀的区别。作为前缀是先运算后返回值,作为后缀是先返回值后运算
在 $((...))
里面使用字符串,Bash 会认为那是一个变量名
1 2 3 4 5 6 7 8 a=5; b=10; c=$(( a * b )); echo $c if (( a < b )); then echo "$a is less than $b " ; fi echo $(( a=1 ))
1 2 3 4 5 6 7 8 9 10 echo "5.01-4*2.0" | bcawk 'BEGIN { print 7.01*5-4.01 }' echo "scale=4; 0.05*0.1" | bcecho "10/3" | bcecho "scale=2; 10/3" | bc | cut -d "." -f1
1 2 3 4 5 6 a=1; echo $a let a++; echo $a let a+=1; echo $a
数组 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 array=(value0 value1) array=( value0 value1 ) array+=(value2) unset array[1]echo ${array[*]} echo ${array} echo ${array[0]} echo ${#array[*]} echo ${#array[0]} ${!array[*]} ${array[@]:position:length} array1=(xxx); array2=(xxx) array_merge=(${array1[*]} ${array2[*]} )
关联数组:使用字符串而不是整数作为数组索引;可等效为字典
1 2 3 4 5 6 7 8 9 10 11 declare -A sounds sounds[dog]="bark" sounds[cow]="moo" echo "${sounds[dog]} " for key in "${!sounds[@]} " ; do echo "$key : ${sounds[$key]} " done
条件控制 if 条件语句 1 2 3 4 5 6 7 8 9 10 if condtion1; then commands elif condition2; then commands fi if condition; then commands; fi
if
结构的判断条件写法:[[]]
是扩展条件判断,相比 []
,支持更多的操作符(如正则表达式匹配)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 test expression [ expression ] [[ expression ]] [[ -z STR ]] [[ -n STR ]] [[ STR1 == STR2 ]] [[ STR1 = STR2 ]] [[ STR1 =~ STR2 ]] [[ -f FILE ]] [[ -d FILE ]] [[ -e FILE ]] [[ NUM1 -eq NUM2 ]] [[ NUM1 -lt NUM2 ]] [[ NUM1 -gt NUM2 ]]
case 分支语句 case
结构用于多值判断,可用到命令行解析中
1 2 3 4 5 6 7 8 9 10 11 case expression in pattern1) commands ;; pattern2) commands ;; ... esac
循环 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 41 for variable in list; do command done for i in 1 2 3 4 5; do echo $i done for i in {1..5}; do echo $i done for i in {1..5..2}; do echo $i done for i in $(seq 1 2 5); do echo $i done for i in $(seq 1 0.2 2); do echo $i done for (( i=1 ; i<=20 ; i++ )); do echo $i done for i in {1..9}; do for j in $(seq $i ); do echo -n -e "$j *$i =$[j*i]\t" done echo done
while 循环 1 2 3 4 5 6 7 8 9 while condition; do command done cat file.txt | while read line; do echo $line done
函数
Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取
函数里面可以用 local
命令声明局部变量
1 2 3 4 5 6 7 8 9 10 11 12 fn () { commands } function fn () { commands } fn
set 命令 set
命令:用于执行脚本时进行调试和错误处理
set -u
:执行脚本时,若遇到不存在的变量,Bash 默认忽略它;在脚本头部加上 set -u
,遇到不存在的变量就会报错,并停止执行
set -x
:在运行结果之前,先输出执行的那一行命令
set -e
:若脚本中有运行失败的命令,Bash 默认会继续执行后面的命令;在脚本头部加上 set -e
,使得脚本只要发生错误,就终止执行;set +e
表示关闭 -e
选项;不适用于管道命令(set -o pipefail
可解决该问题)
set -E
:纠正 set -e
导致的函数内的错误不会被 trap
命令捕获的行为
set -n
:不运行命令,只检查语法是否正确
set -f
:表示不对通配符进行文件名扩展
set -o noclobber
:防止使用重定向运算符 >
覆盖已经存在的文件
1 2 3 4 5 6 7 set set -Eeuxo pipefail set -Eeux set -o pipefail
重定向 1 2 3 4 5 6 7 command > file command >> file command 2> file command 2>&1 command 2> /dev/null command &> /dev/null command < file
Here 文档、字符串:
Here 文档:一种输入多行字符串的方法;本质是重定向
Here 文档内部会发生变量替换,同时支持反斜杠转义,但是不支持通配符扩展,双引号和单引号也失去语法作用,变成了普通字符
Here 字符串:将字符串通过标准输入,传递给命令
1 2 3 4 5 6 7 8 9 << token text token <<< string cat <<< 'hi there' # 等同于 echo 'hi there' | cat
其他
子命令扩展: $()
和``;将命令的输出作为返回值
1 2 echo $(date )echo `date `
1 2 3 4 5 6 7 8 9 10 printf '==%.0s' {1..20}; printf '\n' repeat (){ for i in {1..20}; do echo -n "$1 " ; done } repeat '-' ; echo repeat '=' ; echo
1 2 3 4 5 6 7 8 COMMANDS=("git" "vi" ) for COMMAND in $COMMANDS ; do if ! command -v "$COMMAND " &> /dev/null; then echo "Please install $COMMAND " ; fi done
1 2 3 4 5 6 7 read [-options] [variable...]echo "What is your name?" read NAME echo "Hello, $NAME "
操作历史
退出当前 Shell 的时候,Bash 会将用户在当前 Shell 的操作历史写入 ~/.bash_history
文件
Ctrl + R
快捷键,可以搜索操作历史,选择以前执行过的命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 echo $HISTFILE history history-c export HISTTIMEFORMAT='%F %T ' export HISTSIZE=10000export HISTIGNORE='pwd:ls:exit' !n !-n !! ! + 搜索词 !:p !$ !* !:n
配置项参数终止符 --
:-
和 --
开头的参数,会被 Bash 当作配置项解释;--
的作用是告诉 Bash,在它后面的参数开头的 -
和 --
不是配置项,只能当作实体参数解释
1 2 3 4 5 set -o noclobber echo "xxx" >| file set +o noclobber