Shell 快速上手

引言

Shell 是 linux 系统下非常实用的工具。通过使用 Shell,可以提升在 linux 系统下的工作效率。

Shell 学习

代码都在这里:https://github.com/xiang2017/shell_study

变量

#!/bin/bash
# 变量
echo "01_变量.sh"

# 变量定义与赋值,等号两边不能用空格分开
name=hahahaha
echo $name
echo 也可以使用 {} 输出: ${name}

# 一些特殊变量
test_func() {
  echo "function name is $FUNCNAME"
}
test_func

echo $HOSTNAME
echo $HOSTTYPE
echo $MATCHTYPE
echo $LANG
echo $PWD
# echo $PATH
unset name
echo $name

# 只读变量
readonly R0=100
R0=200
echo $?   # 上一条指令是错误的,所以 $? 为非0

# 变量的作用域
# 变量的作用域又叫“命名空间”,相同名的变量可以在不同命名空间定义。
# 在 Linux 系统中,不同进程 ID 的 Shell 默认为不同的命名空间
VAR_01=100
function update() {
  # 在函数内外访问到的是同一个变量
  VAR_01=200
}
update
echo 变量 VAR_01: $VAR_01

function update02() {
  # 可以使用 local 关键字声明函数内部的局部变量
  local VAR_01=300
}
update02
echo "local 声明的本地变量不会影响全局变量,VAR_01: ${VAR_01}"

# 子 Shell 不会继承变量
echo "echo 子 shell 的 VAR_01 为 \$VAR_01" > tmp.sh
bash ./tmp.sh

# 导出变量(环境变量),子 Shell 可继承,相当于子 Shell 启动时复制了导出的变量
export VAR_01
bash ./tmp.sh

# 在子 Shell 中修改 VAR_01 不会影响

rm ./tmp.sh     # 删除 tmp.sh

转义和引用

#!/bin/bash
# 转义
# 跟其他编程语言里的转义一样,使用转义符 \
echo \# 使用转义输出注释符号 \#
Dollar=123
echo \$Dollar is $Dollar
echo 8 \* 8 = 64

# 引用
# Shell 中一共有 4 中引用符,分别是 双引号,单引号,反引号,转义符

# "" 双引号:部分引用,可以解释变量
echo "\$Dollar is $Dollar"
# 带不带双引号看起来一样,但是对于输出空格有区别
VAR="A     B      C"
echo 不带引号对于连续空格只输出一个:$VAR
echo "带引号会把所有空格输出:$VAR"

# '' 单引号:全引用,只按照字面意思输出内容,转义符也不能用了
echo '$Dollar 在单引号内还是 $Dollar。'
echo '转义符在单引号内输出 \,单引号只把内容作为字面量输出'
echo '转义符不能用,单引号内不能输出单引号'

# `` 反引号:命令替换,将命令的标准输出作为值赋给某个变量
# 命令替换也可以使用 $(命令) 的形式
LS=`ls`
echo "=== LS ==="
echo $LS
echo "=== LS ==="
LSA=$(ls -a)
echo $LSA
# $() 支持嵌套
$(echo $(ls) > tmp.sh)
TMP=$(cat tmp.sh)
echo === tmp ===
echo $TMP
echo === tmp ===
rm tmp.sh

运算符

#!/bin/bash
# 运算符
# Shell 的运算符主要有:
# 比较运算符(整数比较),字符串运算符(字符串测试),文件操作运算符(用于文件测试),逻辑运算符,算术运算符,位运算符,自增自减等

# 算术运算符:加减乘除余幂 以及加等,减等,乘等,初等,余等
A=1
B=2
let "C = $A + $B"  # 需要使用 let 关键字执行运算
echo $C

# 位运算符:左移 右移 按位与 按位或 按位非 按位异或
var1=1
var2=5

let "var = $var1<<2"
echo $var
let "var = $var1&$var2"
echo $var
# 按位非就是 -($var+1)
let "var = ~8"
echo $var

# 自增自减,与其他语言类似,分为前置和后置的区别
var1=1
echo "var1 is $var1"
let "var2=++var1"
echo "var2 前置自增 var1,$var2"
var1=1
let "var2=var1++"
echo "var2 后置自增 var1,$var2"

# 其他算术运算
# 使用 $[] 做运算:$[] 和 $(()) 类似,可用于简单的算术运算
echo '$[1+1]' is $[1+1]
echo '$[5 ** 2]' is $[5 ** 2]

# 使用 expr 做运算:使用 expr 要求操作数和操作符之间用空格分开,否则会被当成字符串
expr 1+1
expr 1 + 1
expr 2 \* 2   # 特殊字符运算符需要转义

# 算术扩展: $((算术表达式))
echo $((2*(1+1)))

# 使用 bc 做运算
# 前面介绍的运算只能基于整数,如果想要计算高精度小数,可以使用 Linux 下的 bc 工具。
# bc 是一款高精度计算语言,支持顺序执行,判断,循环,函数等,下面是一个简单的例子
NUM1=1.2
NUM2=2.3
SUM=$(echo "$NUM1+$NUM2" | bc)
echo $SUM
# 你也可以直接在命令行下输入 bc,然后回车进入 bc 命令行模式

特殊字符

#!/bin/bash
# 特殊字符

# 通配符
# 通配符用于模式匹配,常见的通配符有 *、? 和用 [] 括起来的字符序列。
# 例如:a* 可以匹配以 a 开头的任意长度的字符串,但是不能包含 点号和斜线号
# 所以 a* 不能匹配 abc.txt
# ? 可以匹配任意单个字符
# [] 表示可以匹配其中的任意一个,比如 [abc] 可以匹配a或者b或者c
# [] 中可以用 - 表示起止。比如 [a-z] 匹配所有小写字母
# *? 在 [] 表示普通字符,没有通配功效

# 引号
# 02_转义和引用.sh 中介绍过,主要有单引号,双引号,反引号

# 注释符号

# 大括号
# 大括号 {} 在 Shell 中的用法很多
# 1. 变量扩展 ${PWD}
# 2. 通配符扩展
# 3. 语句块
# 通配符扩展的例子:
touch file_{A,B}
ls . | grep file
rm file_A
rm file_B

# 其他
# 位置参数
# $0: 脚本名本身
# $1,$2... 脚本的第一个参数,第二个参数...
# $# 变量总数
# $* $@ 显示所有参数
# $? 前一个命令的退出的返回值
echo $?    # 正常退出,结果为 0
rm qweqweqweqwe
echo $?    # 出现错误时,结果为 非0
# $! 最后一个后台进程的 ID 号

测试

#!/bin/bash
# 测试:程序运行过程中经常需要根据实际情况执行特定的命令,
# 比如,判断某个文件是否存在,如果不存在,可能需要先创建该文件
# ls tmp.sh
# echo $?

# 测试结构
# 1. test expression   使用 test 指令
# 2. [expression]      使用 []

# 文件测试
# 1. test file_operator FILE
# 2. [file_operator FILE]
test -e tmp.sh
echo $?         # 不存在,上一个指令结果为 1
[ -e tmp.sh ]
echo $?

# 文件测试符,文件不存在时,均返回假
# -b FILE 当文件存在且是块文件时,返回真,否则为假
# -c FILE 当文件存在且是设备文件时,返回真,否则为假
# -d FILE 测试文件是否为目录
# -e FILE 测试文件或目录是否存在
# -f FILE 测试文件是否为普通文件
# -x FILE 判断文件是否为可执行文件
# -w FILE 判断文件可写
# -r FILE 判断文件可读
# -l FILE 判断是否为链接文件
# -p FILE 判断是否为管道文件
# -s FILE 判断文件存在且大小不为 0
# -S FILE 判断是否为 socket 文件
# -g FILE 判断文件是否设置了 SGID
# -u FILE 判断文件是否设置了 SUID
# -k FILE 判断文件是否设置了 sticky 属性
# -G FILE 判断文件属于有效的用户组
# -O FILE 判断文件属于有效的用户
# FILE1 -nt FILE2 FILE1 比 FILE2 新时返回真
# FILE1 -ot FILE2 FILE1 比 FILE2 旧时返回真

# 字符串测试
# 主要包括 等于、不等于、大于、小于、是否为空
# -z string 为空时返回真
echo "字符串测试"
[ -z "" ]
echo '[ -z "" ]' $?   # 结果 0,表示为真

# -n string 非空时返回真
[ -n "aaa" ]
echo '[ -n "aaa" ]' $?
[ "string1" = "string2" ]
echo '[ "string1" = "string2" ]' $?
[ "string1" != "string2" ]
echo '[ "string1" != "string2" ]' $?
[ "string1" > "string2" ]
echo '[ "string1" > "string2" ]' $?
[ "string1" < "string2" ]
echo '[ "string1" < "string2" ]' $?

# 整数比较
# -eq 意 相等
# -gt 意 >
# -lt 意 <
# -ge 意 >=
# -le 意 <=
# -ne 意 !=
[ 1 -eq 2 ]
echo '[ 1 -eq 2 ]' $?
[ 1 -gt 2 ]
echo '[ 1 -gt 2 ]' $?
[ 1 -lt 2 ]
echo '[ 1 -lt 2 ]' $?
[ 1 -ge 2 ]
echo '[ 1 -ge 2 ]' $?
[ 1 -le 2 ]
echo '[ 1 -le 2 ]' $?
[ 1 -ne 2 ]
echo '[ 1 -ne 2 ]' $?

# 逻辑测试符与逻辑运算符
# ! expression 取反
# expression -a expression 同为真,结果为真        
# expression -o expression 只有有一个为真,结果为真
touch tmp.sh
[ ! -e tmp.sh ]
echo '[ ! -e tmp.sh ]' $?

[ -e tmp.sh -a -e tmp1.sh ]
echo '[ -e tmp.sh -a -e tmp1.sh ]' $?

[ -e tmp.sh -o -e tmp1.sh ]
echo '[ -e tmp.sh -o -e tmp1.sh ]' $?

# -a -o 可以用 && 和 || 替代,不过写法上会有区别
[ -e tmp.sh ] && [ -e tmp1.sh ]
echo '[ -e tmp.sh ] && [ -e tmp1.sh ]' $?

[ -e tmp.sh ] || [ -e tmp1.sh ]
echo '[ -e tmp.sh ] || [ -e tmp1.sh ]' $?

rm tmp.sh
rm string2

判断

#!/bin/bash
# bash 的判断与循环与其他语言类似,有 if else elif case

# if 判断结构
# if expression; then
#    command
# elif expression; then
#    command
# else
#    command
# fi
if [ ! -e tmp.sh ];
then
  echo "tmp.sh 不存在,创建它"
  touch tmp.sh

  if [ -e tmp.sh ]; then
    echo "tmp.sh 创建好了"
  else
    echo "tmp.sh 创建失败"
  fi
else
  echo "tmp.sh 存在,删了它"
  rm tmp.sh
fi

# case 判断结构
# case VAR in
# var1) command ;;
# var2) command ;;
# ...
# *) command ;;
# esac
read -p "请输入数字:" NUM
case $NUM in
1) echo "输入为 1" ;;
2) echo "输入为 2" ;;
*) echo "输入为 其他" ;;
esac

rm tmp.sh

循环

#!/bin/bash
# 循环
# Shell 的循环主要有 for、while、until、select 几种

# for 循环
# 带列表的 for 循环:
# for VAR in (list)
# do
#   command
# done
for NUMBER in 1 2 3 4 5
do
  echo $NUMBER
done

fruits="apple banana orange"
for FRUIT in ${fruits}
do
  echo $FRUIT
done

# 循环数字时可以使用 {a..b} 表示从 a 循环到 b
for N in {2..10}
do
  echo $N
done
# 其中 {2..10} 可以用 seq 命令替换
echo "echo with seq:"
for N in $(seq 2 10)
do
  echo $N
done
# seq 命令可以加 “步长”
for N in $(seq 1 2 20)
do
  echo $N
done

# 可以看出,for in 后面的内容可以是任意命令的标准输出
# 比如,我们可以输出当前目录下的所有带 sh 的文件
for VAR in $(ls | grep sh)
do
  echo $VAR
done

# 如果 for 后面没有 in ,则相当于是 in $@
# 你可以执行 bash 07_循环.sh a b c 试一试
for VAR
do
  echo $VAR
done

# 类 C 的 for 循环
# for ((exp1; exp2; exp3))
# do
#   command
# done
for ((i=0, j=100; i < 10; i ++))
do
  echo $i $j
done


# while 循环
# 语法如下:
# while expression
# do
#   command
# done
# while ((1)) 会无限循环
COUNT=0
while [ $COUNT -lt 5 ]
do
  echo $COUNT
  let "COUNT++"
done

# while 按行读取文件
echo "john  30  boy
sue   20  girl" > tmp.txt
while read LINE
do
  NAME=`echo $LINE | awk '{print $1}'`
  AGE=`echo $LINE | awk '{print $2}'`
  SEX=`echo $LINE | awk '{print $3}'`
  echo $NAME $AGE $SEX
done < tmp.txt    # 输入重定向
rm tmp.txt


# until 循环
# until 与 while 类似,区别在于 until 判断为 否,会继续循环,而 while 判断为 真,才继续循环
# until ((0)) 会无限循环
COUNT=0
until [ $COUNT -gt 5 ]
do
  echo $COUNT
  let "COUNT++"
done


# select 循环
# select 是一种菜单式的循环方式,语法结构与 for 相似,每次循环的值由用户选择
echo "choose your menu:"
select MENU in "apple" "banana" "orange" "exit"
do
  echo "you choose $MENU"
  if [[ $MENU = "exit" ]]
  then
    break
  else
    echo "choose again"
  fi
done

# 循环控制,break continue,与其他编程语言一致

函数

#!/bin/bash
# 函数

# 函数的定义
# function FUNCTION_NAME() {
#   command
# }
# 省略 function 关键字
# FUNCTION_NAME() {
#   command
# }            
function func1 {
  echo 1 
}
func2() {
  echo 2 
}

# 函数调用
func1
func2

# 函数返回值
func3 () {
  echo '请输入函数的返回值:'
  read N
  return $N
}
func3
echo "上个函数的返回值是" $?   # 使用 $? 获取上一条指令的返回值

# 函数参数
# 与脚本的参数使用一致
func4 () {
  echo "第一个参数 $1"
  echo "第二个参数 $2"
  echo "所有参数 $@"
  echo "参数数量 $#"
}
func4 a b c

# 使用 set 可以指定位置的脚本(或函数)参数值
func5() {
  set q w e
  echo "参数1 $1"
  echo "所有参数: $@"
}
func5

# 移动位置参数:在 Shell 中可以使用 shift 命令把参数左移一位
func6() {
  while [ $# -gt 0 ]
  do
    echo current \$1 is $1
    shift
  done
}
func6 q w e r t

# 实现一个 pow 函数
pow() {
  let "r=$1**$2"
  return $r

}
pow 2 5
echo $?

重定向

#!/bin/bash
# 重定向
# 重定向是指将原本由标准输入输出的内容,改为输入输出的其他文件或设备

# 系统在启动一个进程时,会为该进程打开三个文件:
# 标准输入(stdin)、标准输出(stdout)、标准错误(stderr)
# 分别用文件标识符 0、1、2 标识
# 如果要为进程打开其他的输入输出,需要从证书 3 开始标识
# 默认情况下,标准输入为键盘,标准输出和标准错误为显示器


# 常见的 IO 重定向符号
# > 标准输出覆盖重定向,将命令的标准输出重定向到其他文件中,会直接覆盖原文件内容
# >> 标准输出追加重定向,将命令的标准输出重定向到其他文件中,不会覆盖文件,会在文件后面追加
# >& 标识输出重定向,讲一个标识的输出重定向到另一个标识的输入
# < 标准输入重定向,命名将从指定文件中读取输入,而不是从键盘中读取输入
# | 管道,从一个命令中读取输出,作为另一个命令的输入


# 输出重定向
# 把原本标准输出到屏幕的内容,重定向到 tmp.txt 文件中
echo "result1" > tmp.txt
cat tmp.txt

echo "result2" > tmp.txt
cat tmp.txt

# 输出追加
echo "输出追加:"
echo "result3" >> tmp.txt
echo "result3" >> tmp.txt
echo "result3" >> tmp.txt
cat tmp.txt
rm tmp.txt

# 标识输出重定向
echo "未重定向标准错误,会直接输出到页面"
# 制定一个不存在的命令
adhfafahdfakdf > tmp.txt
echo "tmp.txt:" `cat tmp.txt`
rm tmp.txt
echo "重定向标准错误到标准输出,会输出到文件中"
asiiaodfuoaf > tmp.txt 2>&1
echo "tmp.txt:" `cat tmp.txt`

# 标准输入重定向
echo "标准输入重定向:"
while read Line
do
  echo $Line
done < tmp.txt

# 管道
# 获取 .sh 文件的名称
ls | grep .sh | cut -f1 -d'.'

# 使用 exec
# exec 是 Shell 的内建命令,执行这个命令时,系统不会启动新的 Shell,而是用被执行的命令替换当前的 Shell 进程
# 因此,在执行完 exec 的命令后,该 Shell 进程将会主动退出
# 例如:执行 exec ls ,后续的其他命令将不会执行。你也可以直接打开 Shell,执行 exec ls 试试
# 此外,exec 还可以用于 I/O 重定向。
# exec < file 将 file 文件中的内容作为 exec 的标准输入
# exec > file 将 file 文件作为标准输出
# exec 3<file 指定文件标识符
# exec 3<&- 关闭文件标识符
# exec 3>file 将写入文件标识符的内容写入到指定文件(输出重定向)
# exec 4<&3 创建文件标识符4,4是3的拷贝 (类似标识输出重定向 2>&1)
# 注:不同的 shell 环境可能会有所差别,比如我在 mac 的 zsh 下就不能正常使用 exec 重定向

# Here Document
# here doc 又称为 此处文档,用于在命令或脚本中按行输入文本。
# 格式为 command << delimiter
# delimiter 是用于标注结束的分隔符

# 示例:
# 你可以在命令行下输入 sort << END 试试
# 你可以在命令行下输入 cat > tmp.txt << END 试试
cat << EOF > tmp.txt
1
2
3
EOF
cat tmp.txt

rm tmp.txt

数组

#!/bin/bash
# 数组

# bash 只支持一维数组

# 定义数组
declare -a mArray
mArray[0]="nihao"
mArray[1]=2

# 定义时赋值,数组的元素用空格分开,其他字符会被当成值,比如 "php", 会被当成 php,
declare -a mArray=("php" "python" 123)

# 数组取值,需要用 ${数组名[索引]} 语法
echo ${mArray[0]}
echo ${mArray[1]}
echo ${mArray[2]}
# 使用 @ * 可以索引全部元素
# @ 得到以空格分开的元素值
# * 得到整个字符串
echo ${mArray[@]}
echo ${mArray[*]}

# 数组长度
echo "数组长度是 ${#mArray[@]}"
echo "数组长度是 ${#mArray[*]}"

# 数组截取
# 可以获取子数组,下面示例为获取数组的第 1、2 下标位置的元素
echo ${mArray[@]: 1:2}
# 可以获取数组中某个元素的若干字符,下面示例为获取数组中第二个元素的 从0开始 3个字符
echo ${mArray[1]: 0:3}

# 合并数组
Front=("javascript" "typescript")   # 数组声明也可以忽略 declear -a
Conn=(${mArray[@]} ${Front[@]})
echo ${Conn[@]}
echo ${#Conn[@]}            # 合并得到数组的长度

# 替换元素
mArray=(${mArray[@] /123/"java"})
echo ${mArray[@]}

# 取消数组或元素
unset mArray[1]
echo "取消下标为 1 的元素后,数组为:${mArray[@]},数组长度为 ${#mArray[@]}"
# 需要注意的是,数组的 1 位置的元素变为了空,而不是后面的元素向前移动
echo "数组 1 位置的元素为 ${mArray[1]}, 2 位置的元素为 ${mArray[2]}"

字符处理

#!/bin/bash
# 字符处理

# 管道
# 从一个命令中读取输出,作为另一个命令的输入
# 示例
# ls | grep .sh | cut -f1 -d'.'

# grep
# grep 是基于行的文本搜索工具,该命令常用的参数有:
# grep [-ivnc] '需要匹配的字符' 文件名
# -i 不区分大小写
# -c 统计包含匹配的行数
# -n 输出行号
# -v 反向匹配
# 其中 '需要匹配的字符' 支持正则表达式模式
grep -in 'func' 01_变量.sh

# sort
# sort 可以对无序的数据进行排序
# sort [-ntkr] 文件名
# -n 采取数字排序
# -t 指定分隔符
# -k 指定第几列
# -r 反向排序

# 示例 使用空格分开每行,按第二列进行排序
echo "3 1 3
1 2 4
5 3 2
1 2 4
5 3 4
2 3 4" | sort -t ' ' -k 2

# uniq
# 使用 uniq 可以删除重复内容
echo "123
123
ab
ab" | uniq

# cut 截取文本
# cut -f指定的列 -d'分隔符'
# 指定的列可以用逗号分隔开,或者使用范围
echo "jhon 10 boy class1
lili 12 girl class2" | cut -f2-4 -d ' '

# tr 做文本转换
# tr '原字符' '目标字符' 其中原字符与目标字符一一对应
head -n 5 01_变量.sh | tr '[a-z]' '[A-Z]'

# paste 进行文本合并
# paste 会把文本按行合并。
# paste -d
echo "1
2
3" > tmp1.txt
echo "a
b
c" > tmp2.txt
paste -d: tmp1.txt tmp2.txt > tmp.txt
cat tmp.txt

# split 分割大文件
# split -l lines file dist_file
# 示例
split -l 5 01_变量.sh split_file
ls | grep split_file
rm split_file*

# sed 与 awk
# ...
# 如果现有工具不能满足你对字符串处理的需求,那就去了解一下 sed 和 awk 命令。

rm tmp*

示例-操作数据库

#!/bin/bash
USER=root
PASSWORD=root

# 使用 -e 执行
databases=`mysql -u$USER -p$PASSWORD -e"show databases"`

for db in $databases
do
  echo "Tables in $db:"
  # 使用 here doc 执行代码块
  mysql -u$USER -p$PASSWORD << EOF
use $db; 
show tables;
EOF
  # 也可以使用输入重定向
  # mysql -u$USER 0pPASSWORD < select.sql
done