首页 / 资讯中心 / 文章详情

从零把 Ubuntu 摸熟:一份写给自己的 Linux 命令手抄本(Shell 篇)

从零把 Ubuntu 摸熟:一份写给自己的 Linux 命令手抄本(Shell 篇) ★ FEATURED ARTICLE
上一篇记的是日常命令这篇把脚本这一层补上。先说一个坑。脚本手动跑得好好的丢进 crontab 里一行输出都没有多半是环境变量不一样cron 的 PATH 比登录 shell 短一截脚本里调用的命令就找不到了。这类问题跟语法无关光看代码看不出来所以这篇的记法是语法和踩过的坑放一起写。顺序按实际用到的来解释器、执行方式、变量、运算、判断、循环、函数最后是 cut、awk 和正则这几个处理文本的工具。一、Shell 是什么Shell 处在用户和内核中间。敲下去的命令由它接住翻成内核能识别的东西再把结果送回终端。它同时是一门脚本语言能判断、能循环、能写函数。Ubuntu 上能当解释器的程序不止一个列一下cat /etc/shells我机器上的输出/bin/sh /bin/bash /usr/bin/bash /bin/rbash /usr/bin/rbash /usr/bin/sh /bin/dash /usr/bin/dash默认用哪个看$SHELL就知道echo $SHELL # /bin/bashUbuntu 默认是 bash。后面的脚本都按 bash 写第一行统一#!/bin/bash。二、第一个脚本怎么跑脚本开头那行#!/bin/bash是指定解释器用的写在文件第一行。#!/bin/bash echo Hello shell!保存成helloworld.sh执行有两条路。一条是把脚本当参数交给 bash脚本本身不需要执行权限sh ./helloworld.sh # 相对路径 sh ~/helloworld.sh # 绝对路径 bash ./helloworld.sh bash ~/helloworld.sh另一条是自己执行得先加 x 权限chmod x helloworld.sh ./helloworld.sh ~/helloworld.sh两者的区别在于谁在执行这个文件。第一种是 bash 读文件内容照着做文件有没有执行权限无所谓第二种是内核直接执行文件缺 x 权限就报 Permission denied。调试的时候用bash 脚本名省事不用反复 chmod要交给 cron 或者别人用再 chmod x。三、变量3.1 系统自带的变量常用的几个是 PATH、HOME、PWD、SHELL、USER。取值写成$变量名美元符和变量名之间不能有空格。echo $PATH # /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin echo $HOME # /home/ubuntu想看当前 shell 里的全部变量直接敲set。3.2 自定义变量A5 # 等号两边不能有空格 echo $A # 5 Amyname # 重新赋值 unset A # 撤销 readonly B2 # 只读改不了也 unset 不掉几条规则都是踩过才知道的变量名由字母、数字、下划线组成不能以数字开头环境变量习惯大写等号两侧不能有空格A 5会报错bash 的变量默认是字符串不能直接算数值里带空格必须加引号C12 echo $C # 12它就是个字符串 DI love linux # 报错找不到命令 love DI like linux # 这样才行 echo $D默认情况下变量只在当前 shell 有效执行脚本时子进程看不到B3 ./helloworld.sh # 脚本里 echo $B什么都不打印 export B ./helloworld.sh # 这次能打印 3测试的时候注意在同一个窗口里做换个终端就是另一个进程了。3.3 特殊变量写脚本离不开这一组。变量含义$nn 是数字$0 是脚本名$1 到 $9 是前九个参数十个以上写成 ${10}$#参数的个数$*所有参数当一个整体看$所有参数每个单独看$?上一条命令的退出状态0 是成功非 0 是失败$*和$的差别在 for 循环那节展开这里先放着。四、算术运算bash 只做整数运算两种写法S$[(23)*4] echo $S # 20 S$(((43)*4)) echo $S # 28$[ ]和$(( ))效果一样后者现在更常见。五、条件判断两种写法test和[ ]后者中括号和条件之间必须有空格不然直接语法错误。这里有个和 C 语言相反的地方刚开始很容易搞混条件成立、数据非空时返回 0成立才是 0不成立返回 1。test abc; echo $? # 0非空算真 test; echo $? # 1空算假 [ abc ]; echo $? # 0 [ ]; echo $? # 1判断条件分三类。整数比较用字母缩写因为[ ]里不能直接写、运算符含义-eq等于-ne不等于-lt小于-le小于等于-gt大于-ge大于等于文件权限的判断是 -r 可读、-w 可写、-x 可执行文件类型是 -e 存在、-f 是普通文件、-d 是目录。多条件用和||前者前一条成功才走后面后者失败才走后面[ abc ] echo OK || echo notOK # OK [ ] echo OK || echo notOK # notOK六、流程控制6.1 if单分支和多分支的骨架if [ 条件 ]; then 程序 fi if [ 条件 ]; then 程序 elif [ 条件 ]; then 程序 else 程序 fi两个细节[和条件之间要有空格if后面也要有空格。一个判断年龄的例子顺手把$#用上#!/bin/bash if [ $# -eq 0 ]; then echo 请携带年龄 elif [ $1 -lt 18 ]; then echo 未成年人 elif [ $1 -lt 60 ]; then echo 成年人 else echo 老年人 fi./if.sh 12 # 未成年人 ./if.sh 34 # 成年人 ./if.sh 66 # 老年人 ./if.sh # 请携带年龄6.2 case选项多的时候 case 比一串 elif 清楚case $变量名 in 值1) 程序1 ;; 值2) 程序2 ;; *) 默认程序 ;; esaccase 行尾是in每个模式以右括号结束;;相当于 break最后的*)是兜底。#!/bin/bash case $1 in 1) echo start ;; 2) echo stop ;; *) echo unknown ;; esac6.3 for第一种是 C 语言风格次数或者范围明确时用它for ((初始值; 循环控制条件; 变量变化)) do 程序 done从 1 加到 100#!/bin/bash sum0 for ((i1; i100; i)) do sum$[$sum$i] done echo $sum # 5050第二种是遍历一组值for 变量 in 值1 值2 值3 ... do 程序 done#!/bin/bash for i in apple banana orange do echo i like $i done6.4 $* 和 $ 的差别不加双引号的时候这两个看起来没区别都是把参数一个个列出来./for3.sh apple banana orange # $* # i like apple # i like banana # i like orange # $ # i like apple # i like banana # i like orange加上双引号就分出高下了。$*把所有参数当成一个整体循环只转一圈$把每个参数当独立的有几个转几圈#!/bin/bash for i in $* do echo i like $i done # 只输出一次i like apple banana orange for j in $ do echo i like $j done # 输出三次apple、banana、orange 各一次传参的脚本里这个坑最常见需要逐条处理参数就得用$。6.5 whilewhile [ 条件 ] do 程序 done同样是 1 加到 100#!/bin/bash sum0 i1 while [ $i -le 100 ] do sum$[$sum$i] i$[$i1] done echo $sum # 5050七、read让脚本接收输入read 把终端输入读进变量read (选项) (参数)常用的两个选项-p给提示符-t是等待秒数不加就一直等。#!/bin/bash read -t 7 -p Enter your name in 7 seconds : NAME echo $NAME七秒内不输入就跳过。放到无人值守的脚本里加个-t比卡在那里强。八、函数Shell 命令是脚本的基本单位函数是把一串操作打包起来反复用。[ function ] funname[()] { Action; [return int;] }function和()可以省一个但不能两个都省。有两点和其他语言不一样。函数必须先声明再调用shell 是逐行执行的不像别的语言先编译一遍。返回值只能通过$?取可以显式return n0 到 255不写就返回最后一条命令的结果。#!/bin/bash sum() { SUM$[$1$2] echo $SUM } read -p 请输入第一个数值: n1 read -p 请输入第二个数值: n2 sum $n1 $n2./fun.sh # 请输入第一个数值: 2 # 请输入第二个数值: 5 # 7九、cut 和 awk9.1 cut按列切cut 是从每行里按位置切数据出来。cut [选项参数] filename选项作用-f取第几列-d指定分隔符默认是制表符只能是一个字符拿 PATH 试一下-f 3-表示从第三列取到结尾echo $PATH | cut -d : -f 3- # /usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin从 ifconfig 的输出里抠 IP也是这么切ifconfig ens33 | grep netmask | cut -d i -f 2 | cut -d -f 2 # 192.168.10.150ifconfig 要先装 net-tools新版 Ubuntu 默认不带。9.2 awk按行分析awk 逐行读文件默认以空格把每行切开再对切开的部分做处理。它比 cut 灵活的地方在于能写条件和计算。awk [选项参数] /pattern1/{action1} /pattern2/{action2}... filename选项作用-F指定分隔符-v定义一个变量传进去几个例子由浅到深# 找 root 开头的行输出第 7 列 awk -F : /^root/{print $7} passwd # /bin/bash # 输出第 1 列和第 7 列中间加逗号 awk -F : /^root/{print $1,$7} passwd # root,/bin/bash # BEGIN 在读数据之前执行一次END 在之后执行一次 awk -F : BEGIN{print user, shell} {print $1,$7} END{print ubuntu,/bin/zsh} passwd # user, shell # daemon,/usr/sbin/nologin # ... # ubuntu,/bin/zsh # -v 把 i1 传进去给用户 id 加 1 awk -v i1 -F : {print $3i} passwd内置变量三个最常用做日志统计离不开变量含义FILENAME文件名NR当前行号NF切开后的列数awk -F : {print filename: FILENAME ,linenum: NR ,col: NF} passwd # filename:passwd,linenum:1,col:7 # filename:passwd,linenum:2,col:7 # 找空行的行号 ifconfig | awk /^$/{print NR} # 9 # 18 # 抠 IP 的 awk 版 ifconfig ens33 | awk -F /inet /{print $2} # 192.168.10.150十、正则表达式正则就是一个字符串去描述一类字符串。grep、sed、awk 都靠它匹配内容。符号含义例子^行首grep ^a 匹配 a 开头的行$行尾grep n$ 匹配 n 结尾的行.任意一个字符r..t 能匹配 root、rabt*前一个字符出现 0 次或多次ro*t 匹配 rt、rot、root[ ]范围内的一个字符[0-9] 数字[68] 是 6 或 8\转义匹配符号本身grep a\$b 匹配含 a$b 的行两个常见的组合^$匹配空行.*匹配任意内容。写校验脚本时可以直接抄的两条邮箱^[a-zA-Z0-9_-][a-zA-Z0-9_-](\.[a-zA-Z0-9_-])$手机号^1[3-9]\d{9}$十一、注释、引号和字符串单行注释以#开头。注意第一行的#!/bin/bash不是注释是给系统看的。#!/bin/bash # 这是注释 A1 # 行尾也能写多行注释用 Here Document 的形式包起来:EOF 这一整段 都会被注释掉 EOFEOF 换成别的标记也行成对就可以。11.1 三种引号的区别写法特点单引号 str原样输出里面的变量不解析中间也不能出现单个单引号双引号 str变量会解析转义字符有效不加引号 str不能有空格有空格会被当成命令切分your_namemyname echo Hello, $your_name! # Hello, $your_name! echo Hello, $your_name! # Hello, myname11.2 字符串的常用操作# 拼接挨着写或者用双引号包变量 greetinghello, $your_name ! greeting_1hello, ${your_name} ! # 长度 stringabcd echo ${#string} # 4 # 从下标 1 开始取 4 个 echo ${string:1:4} # bcd # 查字符位置 echo expr index $string io${变量名}这层大括号是界定边界的。echo $Abar会把 Abar 当成变量名写成echo ${A}bar才输出 A 的值 加 bar。11.3 declare给变量加类型declare -i num10 # 声明成整数 declare -A site # 声明关联数组后面数组那节用十二、参数还有三个前面记的$n、$#、$*、$、$?之外实际写脚本还有三个会用上变量含义$$当前脚本的进程号$!最后一个后台进程的进程号$-当前 shell 的选项跟 set 命令看到的一样十三、数组bash 只有一维数组下标从 0 开始长度不限。# 定义括号里空格分开 array_name(value0 value1 value2 value3) # 也可以逐个赋 array_name[0]value0 array_name[1]value1echo ${array_name[0]} # 第一个元素 echo ${array_name[]} # 全部元素 echo ${array_name[*]} # 全部元素另一种写法 echo ${#array_name[]} # 元素个数 echo ${#array_name[0]} # 第一个元素的长度 echo ${!array_name[]} # 所有下标用字符串当键的叫关联数组得先 declaredeclare -A site site[email]mail.example.com site[blog]www.example.com echo ${site[blog]} # www.example.com echo ${!site[]} # email blog十四、运算符汇总前面条件判断只记了 -eq 那几个这里把算术、关系、布尔、字符串、文件测试一次写全。所有比较都放在[ ]或[[ ]]里做。14.1 算术运算符运算符说明举例加法expr $a $b-减法expr $a - $b*乘法要转义expr $a \* $b/除法expr $b / $a%取余expr $b % $a赋值a$b相等返回 true[ $a $b ]!不等返回 true[ $a ! $b ]expr 是外部命令所以运算符两边必须有空格乘号得写成\*整个表达式要用反引号或者$()包起来才拿得到值。14.2 关系运算符只对数字有效就是 -eq、-ne、-gt、-lt、-ge、-le 那六个。a10 b20 if [ $a -eq $b ]; then echo $a -eq $b: a 等于 b else echo $a -eq $b: a 不等于 b fi14.3 布尔运算符运算符说明!非-o或-a与if [ $a -lt 100 -a $b -gt 15 ]; then echo $a 小于 100 且 $b 大于 15 fi14.4 字符串运算符运算符说明两个字符串相等!两个字符串不相等-z长度为 0 返回 true-n长度不为 0 返回 true$不为空返回 trueaabc if [ -z $a ]; then echo 长度为0; else echo 长度不为0; fi if [ $a ]; then echo 字符串不为空; fi14.5 文件测试运算符说明-b块设备文件-c字符设备文件-d目录-f普通文件-g设置了 SGID 位-k设置了粘着位-p有名管道-u设置了 SUID 位-r可读-w可写-x可执行-s文件不为空-e文件存在-Ssocket-L符号链接14.6 let 和 (( ))no12 let no1 # 自增等价于 no1$[$no11] let no12 # 加 2 (( a )) # (( )) 里不用加 $做算术我倾向用(( ))比 let 和 expr 少踩坑不用管反引号也不用管空格。十五、echo 和 printf15.1 echo参数作用-n输出完不换行-e开启转义\n 换行\t 制表\c 不换行也不输出后面的内容echo -n 不换行 echo -e 换行\n制表\t结束 echo -e 第一行\c几种常见用法# 原样输出不解析 echo $name # 显示命令结果用 $() 比反引号清楚 echo $(date) # 写进文件 覆盖 追加 echo It is a test myfile.txt echo It is a test myfile.txt带颜色的提示echo -e \033[31m这是红色\033[0m echo -e \033[32m这是绿色\033[0m15.2 printfprintf 和 C 里的用法一样做对齐的表格比 echo 好使。printf format-string [arguments...]格式符说明%s字符串%d整数%f浮点数%c字符%x十六进制%o八进制%-10s宽度 10 个字符减号表示左对齐%.2f保留两位小数printf %-10s %-8s %-4s\n 姓名 性别 体重kg printf %-10s %-8s %-4.2f\n 小王 男 66.12 printf %-10s %-8s %-4.2f\n 小李 男 48.65 printf 10 20 %d\n $((1020))和 echo 有一点不同printf 不会自动换行结尾得自己加\n。格式符给少了格式串会重复使用。十六、test 的几种写法test和[ ]是最常见的其实还有[[ ]]和(( ))能力一层比一层强。写法能做什么注意test 条件原始写法等价于 [ 条件 ][ 条件 ]最常用不支持 和 ||要用 -a、-o[[ 条件 ]]支持 、||支持 ~ 正则和通配符bash 扩展不保证 POSIX 兼容(( 表达式 ))纯算术判断内部可以直接写 结果为 0 才算 true# [[ ]] 支持通配符 if [[ abc a* ]]; then echo 以 a 开头; fi if [[ 13800138000 ~ ^1[3-9][0-9]{9}$ ]]; then echo 是手机号; fi # (( )) 里直接用比较符号 a10 if (( a 5 )); then echo 大于 5; fi按场景选纯数字比较用(( ))字符串和文件判断用[[ ]]要兼容老 shell 才退回[ ]。十七、流程控制补充if 里 else 后面不能空着什么都不写会直接语法报错。没内容就别写 else。# 这样是错的 if [ $a -gt 5 ]; then echo 大于 else fi简单的逻辑可以写成一行if [ $a -gt 5 ]; then echo 大于; fi for i in 1 2 3; do echo $i; done无限循环常用这几种while : do command done while true do command done for (( ; ; )) do command doneuntil 和 while 反过来条件成立才退出不成立就一直做until [ 条件 ] do 程序 donea0 until [ ! $a -lt 10 ] do echo $a aexpr $a 1 donebreak 跳出整个循环continue 跳过本次剩下的语句进下一轮while : do echo -n 输入 1 到 5 之间的数字: read aNum case $aNum in 1|2|3|4|5) echo 你输入的数字为 $aNum! ;; *) echo 不在 1 到 5 之间结束 break ;; esac done按行读文件用 while read比for i in $(cat file)稳后者遇到空格会切词while read line do echo $line done filename.txt十八、输入输出重定向三个文件描述符先记住0 是标准输入1 是标准输出2 是标准错误。命令说明command file输出重定向到 file覆盖command file输出追加到 file 末尾command file从 file 读输入n file把描述符 n 的输出写到 filen file同上追加n m输出描述符 m 和 n 合并n m输入描述符 m 和 n 合并 tagtag 之间的内容作为输入输出重定向who users # 结果写进 users终端不再显示 cat users echo hello users # 覆盖 echo hello users # 追加输入重定向注意wc -l users会带文件名wc -l users只从标准输入读不带wc -l users # 2 users wc -l users # 2 command1 infile outfile # 输入输出同时替换标准错误的处理command 2file # 只把 stderr 写进 file command 2file # 追加 command file 21 # stdout 和 stderr 一起写进 file command file1 file2 # stdin 和 stdout 都重定向2是一体的2 和之间加了空格含义就变了。Here Document 把两个标记之间的内容当输入喂给命令command delimiter document delimiter两个规矩结尾的标记必须顶格前面后面都不能有字符包括空格和 Tab开头的标记前后的空格会被忽略。wc -l EOF 第一行 第二行 第三行 EOF # 3#!/bin/bash cat EOF 第一行 第二行 第三行 EOF不想看输出就丢给 /dev/nullcommand /dev/null # 丢掉标准输出 command /dev/null 21 # 标准输出和标准错误一起丢/dev/null 是个特殊文件写进去的东西都消失读它什么也读不到。写 crontab 的时候加一句免得邮箱被日志塞满。十九、文件包含Shell 可以把别的脚本引进来两种写法效果一样. filename # 点和文件名之间有空格 source filename被包含的脚本会在当前脚本里直接执行所以它自己不需要执行权限。# test1.sh #!/bin/bash urlhttp://www.example.com # test2.sh #!/bin/bash . ./test1.sh echo 网址$urlchmod x test2.sh ./test2.sh # 网址http://www.example.com实际项目里一般拿它放公共变量和公共函数几个脚本都 source 同一个 config.sh省得每份都抄一遍。二十、两个值得早点养成的习惯一个是关键步骤打印。逻辑一多就在容易出问题的地方加 echo把变量、参数、路径打出来脚本跑挂了能直接看出停在哪一步。另一个是在脚本开头写上set -euo pipefail。默认情况下脚本里的命令失败了也照样往下走最后返回 0看起来像成功。加上这行命令失败、变量未定义都会立刻退出。调试的时候可以再配合set -x每条命令执行前会先打印出来。#!/bin/bash set -euo pipefail语法这块基本就这些。变量、判断、循环、函数是骨架cut、awk 和正则处理文本重定向和文件包含把脚本跟系统接起来。后面 grep 和 sed 单独再写一篇。
阅读完成 · 觉得有帮助?
咨询建站