Shell编程极简入门

Shell编程不花里胡哨,一篇就够~

一、基础入门

1.1 shell脚本的第一行代码

#!/bin/bash

Linux环境下的任何脚本语言,都是以这样一个被称为shebang的特殊行作为起始的。

1.2 脚本的运行方式

将脚本作为sh的命令行参数

$ sh script.sh

将脚本作为具有执行权限的可执行文件(这种方式shebang行可以不添加)

$ chmod a+x script.sh
$ /Users/xxPath/xxPath/script.sh

1.3 终端打印

$ echo "Hello shell..." # 无情的打印

在终端中生成彩色输出还是比较好玩的,系统日志看起来总那么“无情”,何不给日志来点有温度的颜色

$ echo -e "\033[30m 黑色字 \033[0m"
$ echo -e "\033[31m 红色字 \033[0m"
$ echo -e "\033[32m 绿色字 \033[0m"
$ echo -e "\033[33m 黄色字 \033[0m"
$ echo -e "\033[34m 蓝色字 \033[0m"
$ echo -e "\033[35m 紫色字 \033[0m"
$ echo -e "\033[36m 天蓝字 \033[0m"
$ echo -e "\033[37m 白色字 \033[0m"

1.4 变量的使用

在shell中,每个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式储存。

$ days1=1
$ echo $days1

有时候我们想把一个命令的执行输出结果赋值给一个变量,可以通过反引号字符 `` 或 $() 的格式,比如

$ s1=`date`
$ s2=$(date)

如果shell脚本里头又启动了其它子shell脚本进程的话,很可能涉及环境变量,shell中设置环境变量通过export,使用演示如下:

$ export env_name=env_val

当环境不需要的时候可以取消:

$ unset env_name

1.5 通过shell进行数学运算

在shell中,可以通过let、(( ))、[ ]执行基本的算数操作,如果进行高级操作时,expr和bc这两个工具也会很有用。

#!/bin/bash
num1=1
num2=2
# ------------------- 求和 -----------------
let sum=num1+num2
echo $sum
# ------------------- 自增/自减 -----------------
let num1++
echo $num1
let num1--
echo $num1
# ------------------- 加/减某个值 -----------------
let num1+=1 # 或let num1=num1+1
echo $num1
let num1-=1 # 或let num1=num1-1
echo $num1

# ---- 可以留意到,用let运算,操作变量不需要加$ ----

作为快速入门,要学先做减法,因此这里运算符只简单介绍一种,其他方式可以在真正用到的时候查,大同小异。当然要注意的是:let、(( ))、[ ]、expr这些方式都只能用于整数运算,不支持浮点数,此时bc工具就派上用场了,举个栗子:

$ echo "4 * 5.54" | bc
22.16

1.6 普通数组

# 普通数组赋值的两种方式:
# 1)创建变量时赋值
arr=(t1 t2 t3 t4)

# 2)索引-值赋值
arr[0]="test1"
arr[1]="test2"
arr[2]="test3"
arr[3]="test4"
arr[4]="test5"

# 打印数组所有值的方式
echo ${arr[*]}
echo ${arr[@]}

# 打印数组的长度
echo ${#arr[*]}

1.7 关联数组(可以用字符串作为索引)

关联数组的定义:declare -A ass_array,关联数组从Bash 4.0版本开始被引入,并非所有shell版本支持,看情况用吧

# 关联数组的赋值的两种方式:
# 1)创建变量时赋值
declare -A ass_array 
ass_array=([name1]=ricky1 [name2]=ricky2)

# 2)键值对的形式赋值
declare -A ass_array2 
ass_array2[name3]=ricky3
ass_array2[name4]=ricky4

echo ${ass_array[*]}

1.8 调试脚本

使用选项-x,启动shell脚本,能打印出所执行的每一行命令以及当前状态。

$ sh -x script.sh

当然啦,有时候不需要打印所有行的执行情况,可以用:

set -x : 在执行时显示参数和命令

set +x : 禁止调试

arr=(t1 t2 t3 t4)
set -x
echo ${#arr[*]}
set +x
echo "Ohter code"

在上面的脚本中,仅在-x和+x所限制的区域内的调试信息才会被打印出来,打印结果如下:

$ sh script.sh
+ echo 4
4
+ set +x
Ohter code

1.9 状态码

shell每个命令执行完后都有一个退出状态码,表示它已运行完毕,退出状态码是一个0~255的整数值。在脚本中可以捕获上一个命令的状态码做一些逻辑处理。Linux提供了一个专门的变量$?来保存上个已执行命令的退出状态码,使用演示:

$ date
2020年12月25日 星期五 00时10分02秒 CST
$ echo $?
0

一般命令成功执行时,退出状态码是0,如果命令结束时有错误,退出状态码一般是一个正数值,如:

$ asdfas
zsh: command not found: asdfas
$ echo $?
127

常见退出状态码以及描述(当然,表格里的状态码我没都遇到过,但比如127的退出状态码就比较容易常见了)

状态码 描述
0 命令成功执行
1 一般性未知错误
2 不适合的shell命令
126 命令不可执行
127 没找到命令
128 无效的退出参数
130 通过CTRL+C终止命令
255 正常范围之外的退出状态码

那我们整份脚本文件运行结束的退出状态码呢?


Shell脚本会以脚本中的最后一个命令的退出状态码退出,你也可以改变这种默认行为,返回自己的退出状态码,exit命令就派上用场了:

#!/bin/bash
s1=10
s2=20
let sum=s1+s2
echo $sum
exit 5

假设上述代码为script.sh文件的脚本内容,则在终端的执行结果为:

$ sh script.sh
30
$ echo $?
5

如果我自定义的退出状态码大于前面的0~255的范围呢?比如exit 300(将上述script.sh文件的exit 5改为exit 300),则输出结果为:

$ sh script.sh
30
$ echo $?
$ 44

shell中会对最终结果除以256进行求模,300/256的模为44(即整除后的余数)

1.10 函数的定义和调用

1.10.1 函数的定义和调用
#!/bin/bash

function first_func_demo
{
    echo "函数的定义就这样了..."
}

# 或者

second_func_demo()
{
    echo "函数也可以这么定义..."
}

# 调用函数
first_func_demo
second_func_demo

直接复制上面的代码跑一下吧,一秒就会。如果要向函数传递参数,又如何呢,继续上面的脚本,向函数传递两个参数,演示如下:

function third_func_demo
{
    echo "调用函数时传递参数以及读取参数就这么简单..."
    # 读取第一个参数值
    echo $1
    # 读取第二个参数值
    echo $2
}

# 1) 向函数传递两个参数,如果参数更多,空格分界,以此类推...
num1=10
num2=100
third_func_demo $num1 $num2 

直接复制上面的代码跑一下吧,又是一秒就会。

注意,你可能经常看到shell脚本里头有0、1、2一直到n、@、*、$#...emmm,别慌,总结在下面:

$0:    表示程序名
$1...n:表示第1个...第n个参数
$#:    表示输入参数的个数
$@:    获取传入的参数数组,可用于for循环遍历处理场景
$*:    把所有命令行参数当做一个整体
1.10.2 函数的返回值

shell会把函数当作一个小型脚本,运行结束时会返回一个状态码,默认情况,函数的退出状态码是函数中最后一条命令返回的退出状态码,但这样你就无法得知函数里头的代码是否都被正确的执行,因此我们也可以用return命令返回函数值。

function func1 {
    result=200
    return $result
}

func1
echo "func1 run result: $?"

当然我们也可以通过echo输出来返回函数值

function func2 {
    result=200
    echo $result
}

echo "func2 run result: $(func2)"
1.10.3 在.bashrc文件中定义函数

shell每次启动时会重新载入.bashrc(因为我的Mac是用zsh,所以是.zshrc,为了名字通用,我们都先同意叫bashrc吧。。。),bashrc文件一般放在/home/linux用户名下,我们可以定义自己工具类函数,用于处理日常一些繁琐的工作流程,使其自动化,这里举两个栗子吧:


1.比如每天都要更新所有的git仓库并进行备份;

2.常常很多命令没记错,比如dumpsys package、meminfo、activity,am,pm等等以及其他串口命令,可以自定整理一份文档之后,之后通过调用函数直接打印这些常用命令


下面开始演示,我就定义个简单的栗子吧,在我的.zshrc文件中添加:

# 在.zshrc追加函数
function addem {
    echo $[ $1 + $2 ]
}

重启zsh终端,然后运行试试:

$ addem 10 20
30

1.11 命令行参数的处理

上面展示了调用函数如何输入参数和读取参数,那如果执行某个脚本文件或者执行某个命令行的参数输入、读取呢?其实跟函数参数的输入、读取基本一致,直接上小结吧

1.命令行参数的读取,$0:程序名,$1,$2...表示第一个、第二个参数依次类推
2.特殊参数变量:$#(返回命令参数的个数)
3.获取所有命令行参数:$*(把所有命令行参数当做一个整体)、$@(获取命令行数组,主要用于for循环)

这3点问题不大,直接上个小demo演示,scirpt.sh脚本内容如下:

#!/bin/bash
echo $0
echo $1
echo $2

在终端执行script.sh脚本结果演示:

$ sh /Users/xxx/Desktop/script.sh  HaHa 123
/Users/xxx/Desktop/script.sh
HaHa
123

1.12 命令行选项的处理

命令行选项也没什么特殊的。在命令行上,它们紧跟在脚本名之后,就跟命令行参数一样。实际上,如果愿意,你可以像处理命令行参数一样处理命令行选项。选项是跟在单破折线后面的单个字母,根据不同的选项执行不同的逻辑,有点想switch语句。script.sh演示脚本如下:

#!/bin/bash
while [ -n "$1" ]
do
    case "$1" in
        -a) echo "Found -a option" ;;
        -b) echo "Found -b option" ;;
         *) echo "$1 option not found" ;;
    esac 
    shift
done

在终端执行script.sh脚本并输入选项试试:

$ sh script.sh  -a -b -c -d
Found -a option
Found -b option
-c option not found
-d option not found

那命令行参数和命令行选项同时用呢?通过双破折线(--)来实现参数和选项的分离。shell会用双破折线来表明选项列表的结束,在双破折线之后,脚本就可以放心地读剩下的命令行参数了,而不是当选项处理。

#!/bin/bash
while [ -n "$1" ]
do
    case "$1" in
        -a) echo "Found -a option" ;;
        -b) echo "Found -b option" ;;
        --) shift
            break ;;
         *) echo "$1 option not found" ;;
    esac 
    shift
done

count=1
for param in $@
do
    echo "Param: $param"
    let count+=1
done

我们执行上面的script.sh脚本,并传入选项参数、命令行参数,得到执行结果如下:

$ sh script.sh  -a -b -c -d -- HAHA 123
Found -a option
Found -b option
-c option not found
-d option not found
Param: HAHA
Param: 123

那如果有些选项参数需要传值呢?我们在继续基于上面的脚本修改:

#!/bin/bash
while [ -n "$1" ]
do
    case "$1" in
        -a) echo "Found -a option" ;;
        -b) param_value="$2"
            echo "Found -b option with value $param_value"
            shift ;;
        --) shift
            break ;;
         *) echo "$1 option not found" ;;
    esac 
    shift
done

count=1
for param in $@
do
    echo "Param: $param"
    let count+=1
done

脚本修改之后的执行结果:

$ sh script.sh  -a -b ss -c -d -- HAHA 123
Found -a option
Found -b option with value ss
-c option not found
-d option not found
Param: HAHA
Param: 123

1.13 getopt命令

有时候我们需要合并选项参数,比如这样的格式:

$ sh script.sh -ab

getopt命令可以接受一系列任意形式的命令行选项和参数,并自动将它们转换成适当的格式。getopt的命令格式如下:

getopt optstring params

optstring是关键所在,它定义了命令行所有需要输入的选项参数字母,如果选项参数需要带值,则在对应的字符后面加冒号(:)


有点绕?我们用上面在终端运行的脚本作为栗子:

$ sh script.sh  -a -b ss -c -d -- HAHA 123

一眼就看到script.sh脚本的选项参数有,abcd,其实b还带参数值,那如果用getopt命令来处理后面这一大串选项、参数会是怎样的呢?我们在终端尝试运行下面的命令:

$ getopt ab:cd -a -b ss -cd HAHA 123
-a -b ss -c -d -- HAHA 123

在命令选项的工作机制之后,假设我们以后想了解某个命令行怎么用?可以怎么做呢?还是做个demo吧:

function read_params
{
    while [[ -n $1 ]]
    do
        case $1 in
            -a) echo "Found option -a" ;;
            -b) echo "Found option -b" 
                echo "Value is $2"
                shift ;;
            --) shift
                break ;;
             *) echo "Option not found" ;;
        esac
        shift
    done

    count=1
    for param in $@
    do
        echo $param
        let count+=1
    done
}

read_params `getopt ab: $*`

然后在终端就可以:

$ sh script.sh -ab HAHA 123 456

PS:我们常常用grep命令,比如:

$ grep -ir shell filename
$ grep -i -r shell filename
$ grep -r -i shell filename

相信在了解了getopt命令以及如何shell脚本如何解析命令行选项和命令行参数后,就大概知道上面的grep命令内部是如何解析输入的参数了吧~

二、结构化命令

2.1 if-then的使用

if [ condition ]
then
    cmds
elif [ condition ]
then
    cmds
else
    cmds
fi

# 或者

if [ condition ]; then
    cmds
elif [ condition ]
then
    cmds
else
    cmds
fi

方括号两边各有一个空格,必须加上,否则报错;方括号内可进行数值比较、字符串比较、文件比较。

2.1.1 数值比较
比较 描述
n1 -eq n2 equal
n1 -ge n2 great or equal
n1 -gt n2 great than
n1 -le n2 less or equal
n1 -lt n2 less than
n1 -ne n2 not equal
2.1.2 字符串比较
比较 描述
str1 = str2 str1是否等于str2
str1 != str2 str1是否不等于str2
str1 < str2 str1小于str2
str1 > str2 str1大于str2
-n str1 str1的长度是否不为0
-z str1 str1的长度是否为0

关于字符串大于、小于的比较,我们都知道shell里头有重定向符 > ,因此在做大小比较时需要添加转义,如:

if [ $str1 \> $str2 ]

是不是很麻烦?此时我们可以用if-then的高级特性,双方括号[[ expression ]],那双方括号高级在哪里呢?除了不需要再做转义之外,还可以在双方括号里头写匹配模式,如:

if [[ $USER == r*ky ]]

PS:忘记单方括号的使用吧,直接用双方括号!

2.1.3 文件比较

文件的比较在shell里头用得非常非常的多,也非常的有用!!

比较 描述
-d file 检查file是否存在且是一个目录
-e file 检查file是否存在
-f file 检查file是否存在且是文件
-r file 检查file是否存在且可读
-w file 检查file是否存在且可写
-x file 检查file是否存在且可执行
file1 -nt file2 equal
file1 -ot file2 equal
2.1.4 复合条件
if [ condition1 ] && [ condition2 ]
if [ condition1 ] || [ condition2 ]

2.2 case的使用

case variable in
pattern1 | pattern2) cmds1;;
pattern3) cmds2;;
*) default cmds;;
esac

2.3 for循环的使用

for var in list
do
    cmds
done

2.4 while的使用

while conditions
do
    cmds
done

eg:
var1=10
while [ $var1 -gt 0 ]
do
    echo $var1
    var1=$[ $var1-1 ]
done

2.5 until的使用

until conditions
do
    cmds
done

2.6 break命令

while [ $var1 -lt 10 ] 
do 
    if [ $var1 -eq 5 ] 
    then 
        break 
    fi 
        echo "Iteration: $var1" 
        var1=$[ $var1 + 1 ] 
done

2.7 continue命令

for var1 in 1 2 3 4 5 6 7 8 9 10 
do 
    if [ $var1 -eq 5 ] 
    then 
        continue 
    fi 
        echo "Iteration number: $var1" 
done 

三、好用命令行工具集

$ tr            
$ sed
$ awk
$ cut
$ find 
$ grep
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 从程序员的角度来看, Shell本身是一种用C语言编写的程序,从用户的角度来看,Shell是用户与Linux操作系...
    呼啦啦的爱阅读 223评论 0 1
  • 变量 变量 变量引用:$ 和 ${} 命令替换 命令输出中提取信息,并将其赋给变量反引号 和 $()wu=`...
    wuzsheng阅读 130评论 0 0
  • 1. 摘要 Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语...
    笔名辉哥阅读 719评论 0 13
  • 背景 之前写了系列的shell实战的文章,得到了挺多小伙伴的关注,遇到有些对shell不太熟悉小伙伴,所以有个想法...
    aron1992阅读 821评论 1 1
  • Shell是什么? shell本身是一个命令解释器,介于操作系统的内核(kernel)态和用户态之间,可以执行系统...
    echoworlding阅读 27,848评论 11 81