shell脚本常用命令总结

bash写shell脚本常用的一些命令总结

QQ群:397745473

让代码颜色丰富

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#######color code########
RED="31m"
GREEN="32m"
YELLOW="33m"
BLUE="36m"
FUCHSIA="35m"

colorEcho(){
COLOR=$1
echo -e "\033[${COLOR}${@:2}\033[0m"
}

使用示例:
colorEcho $YELLOW "Please run this script on x86_64 machine."

参考: https://raw.githubusercontent.com/Jrohy/trojan/master/install.sh

通常调用一键远程脚本的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
source <(curl -sL https://git.io/trojan-install)

wget --no-check-certificate -qO AutoDD.sh 'http://git.io/autodd.sh' && bash AutoDD.sh

wget -O install.sh http://download.bt.cn/install/install-ubuntu_6.0.sh && sudo bash install.sh

bash <(curl -s -L https://git.io/v2ray.sh)

bash <(curl -L -s -k "https://git.io/Jvc36")
-k 允许curl使用非安全的ssl连接并且传输数据(证书不受信)

curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

curl -sSL https://get.daocloud.io/docker | sh

wget -qO- https://get.docker.com/ | bash

bash -c "$(curl -fsSL https://raw.githubusercontent.com/kenzok8/Trojan/master/trojan-go_install.sh)"

用date打日志的方法

1
2
3
4
5
6
7
8
date "+【%Y-%m-%d %H:%M:%S】 The configuration file already exists!" 2>&1 | tee -a $logPath

同时会显示日制,并且把日志追加到文件中.
参考:https://raw.githubusercontent.com/vsyour/onekey/main/run.sh


#将获取当前时间(恢复出厂设置)和包括换行符的文字內容輸出到文档
echo -e `date '+%Y-%m-%d %H:%M:%S %A'` "\nHello World !" >> test.txt

获取24小时前的日期存入变量

1
resultsName=$(date -d @$((`date +%s`-3600*24)) '+%Y%m%d')

shell中各种括号的作用

1
2
3
4
5
shell中各种括号的作用()、(())、[]、[[]]、{}

参考: https://www.runoob.com/w3cnote/linux-shell-brackets-features.html
参考: https://blog.csdn.net/taiyang1987912/article/details/39551385
参考: https://blog.csdn.net/tg5156/article/details/19406275

shell 判断是否已安装了某个软件

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
# 判断是否安装了node
if ! type node >/dev/null 2>&1; then
echo 'node 未安装';
else
echo 'node 已安装';
fi


# 判断是否安装了npm
if ! type npm >/dev/null 2>&1; then
Echo_Red 'npm 未安装';
exit 1;
fi


# 在shell中判断某个可执行程序是否存在
# 参考:https://segmentfault.com/q/1010000000156870
$ command -v foo >/dev/null 2>&1 || { echo >&2 "I require foo but it's not installed. Aborting."; exit 1; }

$ type foo >/dev/null 2>&1 || { echo >&2 "I require foo but it's not installed. Aborting."; exit 1; }

$ hash foo 2>/dev/null || { echo >&2 "I require foo but it's not installed. Aborting."; exit 1; }

if which brew 2>/dev/null; then
echo "brew exists!"
else
echo "nope, no brew installed."
fi

shell脚本添加crontab

使用shell脚本或命令行 添加crontab 定时任务

crontab 是运维过程中常用的定时任务执行工具

​ 一般情况下在有新的定时任务要执行时,使用crontab -e ,将打开一个vi编辑界面,配置好后保存退出,但是在自动化运维的过程中往往需要使用shell脚本或命令自动添加定时任务。

接下来结束三种(Centos)自动添加的crontab 任务的方法:

使用 crontab - 命令加载管道输出

1
2
3
4
5
6
7
8
9
10
11
12
# crontab_job="* * * * * echo world"

#添加cron_job定时任务
# ( crontab -l | grep -v "$cron_job"; echo "$cron_job" ) | crontab -

# 删除cron_job定时任务
# ( crontab -l | grep -v"$cron_job" ) | crontab -


另一例子:
CRONTAB_CMD="*/1 * * * * sh $PROGRAM once > /dev/null 2>&1 &"
(crontab -l 2>/dev/null | grep -Fv $PROGRAM; echo "$CRONTAB_CMD") | crontab -

其中,crontab - 可以读取管道传输过来的crontab配置。

将需要添加的定时任务echo到crontab -l的结果后面,再通过管道传输给crontab - 进行加载,便完成了添加;

通过grep -v去除crontab -l 中想要删除的定时任务,再传给crontab -加载配置完成删除。

一条命令就完成了crontab任务的添加和删除,非常方便。

其他更多方法

方法一:

编辑 /var/spool/cron/用户名 文件,如:

1
echo "* * * * * hostname >> /tmp/tmp.txt" >> /var/spool/cron/root

优点:简单

缺点:需要root权限

方法二:

编辑 /etc/crontab 文件,

1
echo "* * * * * root hostname >> /tmp/tmp.txt" >> /etc/crontab

需要注意的是:

与常用的crontab 有点不同,/etc/crontab 需指定用名。

而且该文件定义为系统级定时任务 不建议添加非系统类定时任务,编辑该文件也需要root权限

方法三:

利用crontab -l 加 crontab file 两个命令实现自动添加

1
crontab -l > conf && echo "* * * * * hostname >> /tmp/tmp.txt" >> conf && crontab conf && rm -f conf

由于crontab file会覆盖原有定时任务,所以使用 crontab -l 先导出原有任务到临时文件 “conf” 再追加新定时任务

优点:不限用户,任何有crontab权限的用户都能执行

缺点:稍微复杂

shell 字符串操作(长度,查找,替换)

在做shell批处理程序时候,经常会涉及到字符串相关操作。

有很多命令语句,如:awk,sed都可以做字符串各种操作。

其实shell内置一系列操作符号,可以达到类似效果,大家知道,

使用内部操作符会省略启动外部程序等时间,因此速度会非常的快。

1
原文参考:https://www.cnblogs.com/chengmo/archive/2010/10/02/1841355.html

一、判断读取字符串值

表达式 含义
${var} 变量var的值, 与$var相同
${var-DEFAULT} 如果var没有被声明, 那么就以$DEFAULT作为其值 *
${var:-DEFAULT} 如果var没有被声明, 或者其值为空, 那么就以$DEFAULT作为其值 *
${var=DEFAULT} 如果var没有被声明, 那么就以$DEFAULT作为其值 *
${var:=DEFAULT} 如果var没有被声明, 或者其值为空, 那么就以$DEFAULT作为其值 *
${var+OTHER} 如果var声明了, 那么其值就是$OTHER, 否则就为null字符串
${var:+OTHER} 如果var被设置了, 那么其值就是$OTHER, 否则就为null字符串
${var?ERR_MSG} 如果var没被声明, 那么就打印$ERR_MSG *
${var:?ERR_MSG} 如果var没被设置, 那么就打印$ERR_MSG *
${!varprefix*} 匹配之前所有以varprefix开头进行声明的变量
${[email protected]} 匹配之前所有以varprefix开头进行声明的变量

加入了“*” 不是意思是: 当然, 如果变量var已经被设置的话, 那么其值就是$var.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[[email protected] ~]$ echo ${abc-'ok'}
ok
[[email protected] ~]$ echo $abc

[[email protected] ~]$ echo ${abc='ok'}
ok
[[email protected] ~]$ echo $abc
ok



如果abc 没有声明“=" 还会给abc赋值。

[[email protected] ~]$ var1=11;var2=12;var3=
[[email protected] ~]$ echo ${[email protected]}
var1 var2 var3
[[email protected] ~]$ echo ${!v*}
var1 var2 var3



${!varprefix*}${[email protected]}相似,可以通过变量名前缀字符,搜索已经定义的变量,无论是否为空值。

二、字符串操作(长度,读取,替换)

表达式 含义
| $string的长度 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|                                        |                                                              |
| ```${string:position}``` | 在$string中, 从位置$position开始提取子串 |
| ```${string:position:length}``` | 在$string中, 从位置$position开始提取长度为$length的子串 |
| | |
| ```${string#substring}``` | 从变量$string的开头, 删除最短匹配$substring的子串 |
| ```${string##substring}``` | 从变量$string的开头, 删除最长匹配$substring的子串 |
| ```${string%substring}``` | 从变量$string的结尾, 删除最短匹配$substring的子串 |
| ```${string%%substring}``` | 从变量$string的结尾, 删除最长匹配$substring的子串 |
| | |
| ```${string/substring/replacement}``` | 使用$replacement, 来代替第一个匹配的$substring |
| ```${string//substring/replacement}``` | 使用$replacement, 代替*所有*匹配的$substring |
| ```${string/#substring/replacement}``` | 如果$string的*前缀*匹配$substring, 那么就用$replacement来代替匹配到的$substring |
| ```${string/%substring/replacement}``` | 如果$string的*后缀*匹配$substring, 那么就用$replacement来代替匹配到的$substring |
| | |





```bash
说明:$substring 可以是一个*正则表达式*.
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
42
43
44
45
46
47
48
49
50
51
52
53
**1.长度**

[[email protected] ~]$ test='I love china'
[[email protected] ~]$ echo ${#test}
12

${#变量名}得到字符串长度



**2.截取字串**

[[email protected] ~]$ test='I love china'
[[email protected] ~]$ echo ${test:5}
e china
[[email protected] ~]$ echo ${test:5:10}
e china

${变量名:起始:长度}得到子字符串



**3.字符串删除**

[[email protected] ~]$ test='c:/windows/boot.ini'
[[email protected] ~]$ echo ${test#/}
c:/windows/boot.ini
[[email protected] ~]$ echo ${test#*/}
windows/boot.ini
[[email protected] ~]$ echo ${test##*/}
boot.ini

[[email protected] ~]$ echo ${test%/*}
c:/windows
[[email protected] ~]$ echo ${test%%/*}

${变量名#substring正则表达式}从字符串开头开始配备substring,删除匹配上的表达式。

${变量名%substring正则表达式}从字符串结尾开始配备substring,删除匹配上的表达式。

注意:${test##*/},${test%/*} 分别是得到文件名,或者目录地址最简单方法。

**4.字符串替换**

[[email protected] ~]$ test='c:/windows/boot.ini'
[[email protected] ~]$ echo ${test/\//\\}
c:\windows/boot.ini
[[email protected] ~]$ echo ${test//\//\\}
c:\windows\boot.ini



${变量/查找/替换值} 一个“/”表示替换第一个,”//”表示替换所有,当查找中出现了:”/”请加转义符”\/”表示。

三、性能比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在shell中,通过awk,sed,expr 等都可以实现,字符串上述操作。下面我们进行性能比较。

[[email protected] ~]$ test='c:/windows/boot.ini'
[[email protected] ~]$ time for i in $(seq 10000);do a=${#test};done;

real 0m0.173s
user 0m0.139s
sys 0m0.004s

[[email protected] ~]$ time for i in $(seq 10000);do a=$(expr length $test);done;

real 0m9.734s
user 0m1.628s



速度相差上百倍,调用外部命令处理,与内置操作符性能相差非常大。在shell编程中,尽量用内置操作符或者函数完成。使用awk,sed类似会出现这样结果。

30个Linux Shell脚本经典案例(上)

1
原文地址: https://zhuanlan.zhihu.com/p/161356277

编写Shell过程中注意事项:

  1. 开头加解释器:#!/bin/bash
  2. 语法缩进,使用四个空格;多加注释说明。
  3. 命名建议规则:变量名大写、局部变量小写,函数名小写,名字体现出实际作用。
  4. 默认变量是全局的,在函数中变量local指定为局部变量,避免污染其他作用域。
  5. 有两个命令能帮助我调试脚本:set -e 遇到执行非0时退出脚本,set-x 打印执行过程。
  6. 写脚本一定先测试再到生产上。

1.获取随机字符串或数字

获取随机8位字符串:

1
2
3
4
5
6
7
8
9
方法1:
# echo $RANDOM |md5sum |cut -c 1-8
471b94f2
方法2:
# openssl rand -base64 4
vg3BEg==
方法3:
# cat /proc/sys/kernel/random/uuid |cut -c 1-8
ed9e032c

获取随机8位数字:

1
2
3
4
5
6
7
8
9
方法1:
# echo $RANDOM |cksum |cut -c 1-8
23648321
方法2:
# openssl rand -base64 4 |cksum |cut -c 1-8
38571131
方法3:
# date +%N |cut -c 1-8
69024815

cksum:打印CRC效验和统计字节

2.定义一个颜色输出字符串函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
方法1:
function echo_color() {
if [ $1 == "green" ]; then
echo -e "\033[32;40m$2\033[0m"
elif [ $1 == "red" ]; then
echo -e "\033[31;40m$2\033[0m"
fi
}

方法2:
function echo_color() {
case $1 in
green)
echo -e "[32;40m$2[0m"
;;
red)
echo -e "[31;40m$2[0m"
;;
*)
echo "Example: echo_color red string"
esac
}

使用方法:echo_color green “test” function关键字定义一个函数,可加或不加。

3.批量创建用户

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

DATE=$(date +%F_%T)
USER_FILE=user.txt

echo_color(){
if [ $1 == 'green' ]; then
echo -e "[32;40m$2[0m"
elif [ $1 == 'red' ]; then
echo -e "[31;40m$2[0m"
fi
}

# 如果用户文件存在并且大小大于0就备份
if [ -s $USER_FILE ]; then
mv $USER_FILE $USER_FILE-$DATE.bak
echo_color green "$USER_FILE exist, rename ${USER_FILE}-${DATE}.bak"
fi

echo -e "User Password" >> $USER_FILE
echo "-------------------">> $USER_FILE

for USER in user{1..10}; do
if ! id $USER &>/dev/null; then
PASS=$(echo $RANDOM |md5sum |cut -c 1-8)
useradd $USER
echo $PASS |passwd --stdin $USER &> /dev/dell
echo -e "$USER $PASS" >> $USER_FILE
echo "$USER User create successful."
else
echo_color red "$USER User already exists!"
fi
done

4.检查软件包是否安装

1
2
3
4
5
6
#!/bin/bash
if rpm -q sysstat &>/dev/null; then
echo "sysstat is already installed."
else
echo "sysstat is not installed!"
fi

5.检查服务状态

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

PORT_C=$(ss -ant |grep -c 6443)
PS_C=$(ps -ef |grep kube-apiserver |grep -vc grep)

if [ $PORT_C -eq 0 -o $PS_C -eq 0 ]; then
echo "kube-apiserver service dowmped"
else
echo "kube-apiserver service running!"
fi

6.检查主机存活状态

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
42
43
44
45
46
47
48
49
50
51
方法1:将错误IP放到数组里面判断是否ping失败三次
#!/bin/bash
for IP in $IP_LIST; do
NUM=1
while [ $NUM -le 3 ]; do
if ping -c 1 $IP &> /dev/null; then
echo "$IP Ping is successful."
break
else
FAIL_COUNT[$NUM]=$IP
let NUM++
fi
done
if [ ${#FAIL_COUNT[*]} -eq 3 ]; then
echo "${FAIL_COUNT[1]} Ping is failure!"
unset FAIL_COUNT[*]
fi
done

方法2:将错误次数放到FAIL_COUNT变量里面判断是否ping失败三次
#!/bin/bash
for IP in $IP_LIST; do
FAIL_COUNT=0
for (( i=1;i<=3;i++)); do
if ping -c 1 $IP &>/dev/null; then
echo "$IP Ping is successful."
break
else
let FAIL_COUNT++
fi
done
if [ $FAIL_COUNT -eq 3 ]; then
echo "$IP Ping is failure!"
fi
done

方法3:利用for循环将ping通就跳出循环继续,如果不跳出就会走到打印ping失败
#!/bin/bash
ping_success_status() {
if ping -c 1 $IP &>/dev/null; then
echo "$IP Ping is successful."
continue
fi
}

for IP in $IP_LIST; do
ping_success_status
ping_success_status
ping_success_status
echo "$IP Ping is failure!"
done

7.监控CPU、内存和硬盘利用率

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
42
43
44
45
46
47
48
49
#!/bin/bash

DATE=$(date +%F" "%H:%M)
IP=$(ifconfig eth0 |awk '/netmask/ {print $2}')
if ! which vmstat &> /dev/null; then
echo "vmstat command no found, Please install procps package."
exit 1
fi

## CPU
US=$(vmstat |awk 'NR==3 {print $13}')
SY=$(vmstat |awk 'NR==3 {print $14}')
IDLE=$(vmstat |awk 'NR==3 {print $15}')
WAIT=$(vmstat |awk 'NR==3 {print $15}')
USE=$(($US+$SY))
if [ $USE -ge 50 ];then
echo "
Date: $DATE
Host: $IP
Problem: CPU utilization $USE"
fi

## Mem
TOTAL=$(free -m |awk '/Mem/ {print $2}')
USE=$(free -m |awk '/Mem/ {print $3}')
FREE=$(free -m |awk '/Mem/ {print $4+$6}')

if [ $FREE -lt 1024 ]; then
echo "
Date: $DATE
Host: $IP
Problem: Total=$TOTAL,Use=$USE,Free=$FREE"
fi

#disk
PART_USE=$(df -h |awk -F'[% ]+' 'BEGIN{OFS="="} /^\/dev/ {print $1,$2,$5,$6}')
for i in $PART_USE; do
PART=$(echo $i |cut -d"=" -f1)
TOTAL=$(echo $i |cut -d "=" -f2)
USE=$(echo $i |cut -d"=" -f3)
MOUNT=$(echo $i |cut -d"=" -f4)
if [ $USE -gt 80 ]; then
echo "
Date: $DATE
Host: $IP
Total: $TOTAL
Problem: $PART=$USE($MOUNT)"
fi
done

8.批量主机磁盘利用率监控

前提监控端和被监控端SSH免交互登录或者密钥登录。 写一个配置文件保存被监控主机SSH连接信息,文件内容格式:IP User Port

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
HOST_INFO=host.info
for IP in $(awk '/^[^#]/ {print $1}' $HOST_INFO); do
USER=$(awk -v ip=$IP 'ip==$1 {print $2}' $HOST_INFO)
PORT=$(awk -v ip=$IP 'ip==$1 {print $3}' $HOST_INFO)
TMP_FILE=/tmp/disk.tmp
ssh -p $PORT $USER@$IP df -h > $TMP_FILE
USE_RATE_LIST=$(awk 'BEGIN{OFS="="} /^\/dev/ {print $NF,int($5)}' $TMP_FILE)

for USE_RATE in $USE_RATE_LIST; do
PART_NAME=${USE_RATE%=*} ##从右到左,非贪婪匹配,匹配到的删除
USE_RATE=${USE_RATE#*=} ##从左到右,非贪婪匹配,匹配到的删除
if [ $USE_RATE -ge 10 ];then
echo "Warning: $IP $PART_NAME Partition usage $USE_RATE%!"
fi
done
done

9.检查网站可用性

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/bin/bash

#--------------------
#1)检查URL可用性
#方法1:
check_url() {
HTTP_CODE=$(curl -o /dev/null --connect-timeout 3 -s -w "%{http_code}" $1)
if [ $HTTP_CODE -ne 200 ]; then
echo "Warning: $1 Access failure!"
fi
}
#方法2:
check_url_2() {
if ! wget -T 10 --tries=1 --spider $1 &>/dev/null; then
#-T超时时间,--tries尝试1次,--spider爬虫模式
echo "Warning: $1 Access failure!"
fi
}

#check_url www.baidu.com
#check_url_2 www.aaaa.com

#2)判断三次URL可用性
#思路与上面检查主机存活状态一样。

#---------------------------------

URL_LIST="www.baidu.com www.agasgf.com"

#------
#方法1:利用循环技巧,如果成功就跳出当前循环,否则执行到最后一行
check_url_3() {
HTTP_CODE=$(curl -o /dev/null --connect-timeout 3 -s -w "%{http_code}" $1)
if [ $HTTP_CODE -eq 200 ]; then
continue
fi
}

for URL in $URL_LIST; do
check_url_3 $URL
check_url_3 $URL
check_url_3 $URL
echo "Warning: $URL Access failure!"
done

#------
#方法2:错误次数保存到变量

for URL in $URL_LIST; do
FAIL_COUNT=0
for ((i=1;i<=3;i++)); do
HTTP_CODE=$(curl -o /dev/null --connect-timeout 3 -s -w "%{http_code}" $URL)
if [ $HTTP_CODE -ne 200 ]; then
let FAIL_COUNT++
else
break
fi
done
if [ $FAIL_COUNT -eq 3 ]; then
echo "Warning: $URL Access failure!"
fi
done

#------
#方法3:错误次数保存到数组
for URL in $URL_LIST;do
NUM=1
unset FAIL_COUNT
while [ $NUM -le 3 ]; do
HTTP_CODE=$(curl -o /dev/null --connect-timeout 3 -s -w "%{http_code}" $URL)
if [ $HTTP_CODE -ne 200 ]; then
FAIL_COUNT[$NUM]=$URL
let NUM++
else
break
fi
done
if [ ${#FAIL_COUNT[@]} -eq 3 ];then
echo "Warning: $URL Access failure!"
fi
done

10.检查MySQL主从同步状态

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash  
USER=bak
PASSWD=123456
IO_SQL_STATUS=$(mysql -u$USER -p$PASSWD -e show slave statusG |awk -F: /Slave_.*_Running/{gsub(": ",":");print $0} ) #gsub去除冒号后面的空格
for i in $IO_SQL_STATUS; do
THREAD_STATUS_NAME=${i%:*}
THREAD_STATUS=${i#*:}
if [ "$THREAD_STATUS" != "Yes" ]; then
echo "Error: MySQL Master-Slave $THREAD_STATUS_NAME status is $THREAD_STATUS!"
fi
done

Shell编程必备的基础知识

在linux下使用shell编程常常能够极大简化我们的工作。本文总结了Shell编程必备的基础知识。

入参和默认变量

对于shell脚本而言,有些内容是专门用于处理参数的,它们都有特定的含义,例如:

1
2
3
/home/shouwang/test.sh para1 para2 para3
$0 $1 $2 $3
脚本名 第一个参数 第三个参数 第三个参数

除此之外,还有一些其他的默认变量,例如:

1
2
3
4
5
$#  代表脚本后面跟的参数个数,前面的例子中有3个参数
[email protected] 代表了所有参数,并且可以被遍历
$* 代表了所有参数,且作为整体,和$*很像,但是有区别
$$ 代表了当前脚本的进程ID
$? 代表了上一条命令的退出状态

变量

给变量赋值,使用等号即可,但是等号两边千万不要有空格,等号右边有空格的字符串也必须用引号引起来:

1
para1="hello world"  #字符串直接赋给变量para1

unset用于取消变量。例如:

1
unset para1

如何使用变量呢?使用变量时,需要在变量前加$,例如要打印前面para1的内容:

1
2
echo "para1 is $para1"
#将会输出 para1 is hello world

或者变量名两边添加大括号:

1
2
echo "para1 is ${para1}!"
#将会输出 para1 is hello world!

命令执行

在shell中执行命令通常只需要像在终端一样执行命令即可,不过,如果想要命令结果打印出来的时候,这样的方式就行不通了。因此,shell的命令方式常有:

1
a=`ls`   #`是左上角~键,不是单引号

或者使用$,后面括号内是执行的命令:

1
echo "current path is $(pwd)"   #

另外,前面两种方式对于计算表达式也是行不通的,而要采取下面的方式:

1
echo "1+1=$((1+1))"  #打印:1+1=2

即$后面用两重括号将要计算的表达式包裹起来

那如果要执行的命令存储在变量中呢?前面的方法都不可行了,当然括号内的内容被当成命令执行还是成立的。要使用下面的方式,例如:

1
2
a="ls"
echo "$($a)"

但是如果字符串时多条命令的时候,上面的方式又不可行了,而要采用下面的方式:

1
2
a="ls;pwd"
echo "$(eval $a)"

这是使用了eval,将a的内容都作为命令来执行。

条件分支

一般说明,如果命令执行成功,则其返回值为0,否则为非0,因此,可以通过下面的方式判断上条命令的执行结果:

1
2
3
4
5
6
7
8
9
if [ $? -eq 0 ]
then
echo "success"
elif [ $? -eq 1 ]
then
echo "failed,code is 1"
else
echo "other code"
fi

case语句使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
name="aa"
case $name in
"aa")
echo "name is $name"
;;
"")
echo "name is empty"
;;
"bb")
echo "name is $name"
;;
*)
echo "other name"
;;
esac

初学者特别需要注意以下几点:

1
2
3
4
+ []前面要有空格,它里面是逻辑表达式
+ if elif后面要跟then,然后才是要执行的语句
+ 如果想打印上一条命令的执行结果,最好的做法是将$?赋给一个变量,因为一旦执行了一条命令$?的值就可能会变。
+ case每个分支最后以两个分号结尾,最后是case反过来写,即esac。

多个条件如何使用呢,两种方式,方式一:

1
2
3
if [ 10 -gt 5 -o 10 -gt 4 ];then
echo "10>5 or 10 >4"
fi

方式二:

1
2
3
if [ 10 -gt 5 ] || [ 10 -gt 4 ];then
echo "10>5 or 10 >4"
fi

其中-o或者||表示或。这里也有一些常见的条件判定。

总结如下:

  • -o or 或者,同||
  • -a and 与,同&&
  • ! 非

整数判断:

  • -eq 两数是否相等
  • -ne 两数是否不等
  • -gt 前者是否大于后者(greater then)
  • -lt 前面是否小于后者(less than)
  • -ge 前者是否大于等于后者(greater then or equal)
  • -le 前者是否小于等于后者(less than or equal)

字符串判断str1 exp str2:

  • -z “$str1” str1是否为空字符串
  • -n “$str1” str1是否不是空字符串
  • “$str1” == “$str2” str1是否与str2相等
  • “$str1” != “$str2” str1是否与str2不等
  • “$str1” =~ “str2” str1是否包含str2

特别注意,字符串变量最好用引号引起来,因为一旦字符串中有空格,这个表达式就错了,有兴趣的可以尝试当str1=”hello world”,而str2=”hello”的时候进行比较。

文件目录判断:filename

  • -f $filename 是否为文件
  • -e $filename 是否存在
  • -d $filename 是否为目录
  • -s $filename 文件存在且不为空
  • ! -s $filename 文件是否为空

循环

循环形式一,和Python的for in很像:

1
2
3
4
#遍历输出脚本的参数
for i in [email protected]; do
echo $i
done

循环形式二,和C语言风格很像:

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

循环打印0到9。

循环形式三:

1
2
3
for i in {1..5}; do
echo "Welcome $i"
done

循环打印1到5。

循环方式四:

1
2
3
4
while [ "$ans" != "yes" ]
do
read -p "please input yes to exit loop:" ans
done

只有当输入yes时,循环才会退出。即条件满足时,就进行循环。

循环方式五:

1
2
3
4
5
ans=yes
until [ $ans != "yes" ]
do
read -p "please input yes to exit loop:" ans
done

这里表示,只有当ans不是yes时,循环就终止。

循环方式六:

1
2
3
for i in {5..15..3}; do
echo "number is $i"
done

每隔5打印一次,即打印5,8,11,14。

函数

定义函数方式如下:

1
2
3
4
myfunc() 
{
echo "hello world $1"
}

或者:

1
2
3
4
function myfunc() 
{
echo "hello world $1"
}

函数调用:

1
2
para1="shouwang"
myfunc $para1

返回值

通常函数的return返回值只支持0-255,因此想要获得返回值,可以通过下面的方式。

1
2
3
4
5
function myfunc() {
local myresult='some value'
echo $myresult
}
val=$(myfunc) #val的值为some value

通过return的方式适用于判断函数的执行是否成功

1
2
3
4
5
6
7
8
9
10
function myfunc() {

#do something
return 0
}
if myfunc;then
echo "success"
else
echo "failed"
fi

注释

shell通过#来注释一行内容,前面我们已经看到过了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# 这是一行注释
:'
这是
多行
注释
'
ls

:<<EOF
这也可以
达到
多行注释
的目的
EOF

日志保存

脚本执行后免不了要记录日志,最常用的方法就是重定向。以下面的脚本为例:

1
2
3
4
#!/bin/bash
#test.sh
lll #这个命令是没有的,因此会报错
date

方式一,将标准输出保存到文件中,打印标准错误:

1
./test.sh > log.dat

这种情况下,如果命令执行出错,错误将会打印到控制台。所以如果你在程序中调用,这样将不会讲错误信息保存在日志中。

方式二,标准输出和标准错误都保存到日志文件中:

1
./test.sh > log.dat 2>&1

2>&1的含义可以参考《如何理解linuxshell中的2>&1

方式三,保存日志文件的同时,也输出到控制台:

1
./test.sh |tee log.dat

脚本执行

最常见的执行方式前面已经看到了:

1
./test.sh

其它执行方式:

1
2
3
4
sh test.sh  #在子进程中执行
sh -x test.sh #会在终端打印执行到命令,适合调试
source test.sh #test.sh在父进程中执行
. test.sh #不需要赋予执行权限,临时执行

脚本退出码

很多时候我们需要获取脚本的执行结果,即退出状态,通常0表示执行成功,而非0表示失败。为了获得退出码,我们需要使用exit。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
function myfun()
{
if [ $# -lt 2 ]
then
echo "para num error"
exit 1
fi
echo "ok"
exit 2
}
if [ $# -lt 1 ]
then
echo "para num error"
exit 1
fi

returnVal=`myfun aa`
echo "end shell"
exit 0

这里需要特别注意的一点是,使用

1
returnVal=`myfun aa`

这样的句子执行函数,即便函数里面有exit,它也不会退出脚本执行,而只是会退出该函数,这是因为exit是退出当前进程,而这种方式执行函数,相当于fork了一个子进程,因此不会退出当前脚本。最终结果就会看到,无论你的函数参数是什么最后end shell都会打印。

1
2
./test.sh;echo $?
0

这里的0就是脚本的执行结果。

总结

以上就是shell编程最基本也是最关键的内容。当然这并非全部,例如数组,字典,参数处理等都没有详细介绍,由于篇幅有限,将会在后面的文章中进行详细介绍。

QQ群:397745473

知识星球