【万字长文】手摸手教你shell脚本编程
我写这篇文章的目的
前段时间参加了联创团队的春令营, 为期半个多月的春令营做了三个项目, 其中有一个项目是关于shell的, 当时完全没接触过shell脚本编程, 网上相关的资料有, 但是不多, 而且相对系统一些的教程却只是介绍了一些基本语法而已, 实战中还是觉得异常困难, 其实就我个人而言, 我是更喜欢那种在实战中运用并讲解知识点的文章的, 但是很可惜, 貌似没有酱紫的文章, 于是萌生了写一篇以实战为主的shell教程的想法.
「阅读须知」: 其实我和正在看这篇文章的你一样, 也是一个学习者的身份, 是学道者, 而不是布道者, 所以文章中难免会有许多的错误, 或者有一些代码写的挺蠢的, 希望大家批评指正, 另, 文末有一个小福利哈!😊
如果对shell完全没有基础的童鞋, 那么可以先看看我的shell笔记!
项目概述
这个项目相当于一个人口管理系统, 但是背景是古代的, 所以有一些比较奇怪的需求, 比如头衔继承, 男女不平等等
「详细介绍如下所示:」
来自中世纪莱昂王国的阿方索国王由于处理宫廷事务心力憔悴,繁冗复杂的人脉关系让他失落沮丧,于是为了减少工作压力,更多的关注 家 庭 生 活 ,阿方索国王花费了重金(?)雇佣你来为他理清各个领主之间的关系。
但是光是伊比利亚地区的各个封臣都已经是恒河沙数了,要统计清整个欧洲的家族爵位关系显然是人力难为的,机智的你想到了利用计算机来帮阿方索国王解决他的问题。
由于当时的年代过于久远,没有c\c++\python之类的语言,你被推荐使用shell脚本来完成任务
而且因为阿方索国王是中世纪的人,对计算机那一套理论并不熟悉,所以你被期望开发出的脚本使用上亲民一点。
对于每一个人民, 他有着如下的信息: 这个json文件的格式挺重要的, 看后面的代码时, 可以拿这个json文件对照着看
{
"name":"Alfonso VI", //姓名
"id": 1, //任意的不重复id
"sex":"male", //性别
"couples": [2, 3], //配偶 "Constance of Burgundy"与"Zaida of Seville"
"family": "Jimena", //家族 家族都是独一无二的
"children": [4, 5], //子嗣 "Urraca of León"与”Sancho Alfónsez“
"maintitle": "Kingdom of León", //主头衔
"titles": ["Kingdom of León", "Lord of Salamanca"] //所有头衔
}
其中头衔和家族字段可以不存在
由于这个项目的需求大部分是互不影响, 可以分开实现的, 所以我会在后文中罗列出这些需求以及相应的解决思路和核心的代码
项目实战
需求: 首先,阿方索国王希望将每一个人的信息都存放在一个「数据文件夹」内。
同时你的脚本应该能够支持以下操作:
-
迁移数据至另一个文件夹 -
改变脚本的 「数据文件夹」(这意味着如果之前脚本将个人信息存放在文件夹A下,之后将 「数据文件夹」换成B,那么对于之后的所有操作里的个人信息均是B文件夹内的信息) -
备份数据并打包,可以指定路径,如果未指定,打包到当前目录下
实战:
-
对于迁移数据至另一个文件夹的这一个需求, 他的意思就是所有人的信息都要放在一个单独的文件夹下面, 其实就是类似数据库这么一个概念, 这个数据文件夹就相当于一个数据库
创建数据文件夹的操作如下所示
dataDir="${home}/data"
#如果数据文件夹不存在, 那么就创建一个
if [ ! -d "$dataDir" ]; then
mkdir "$dataDir"
chmod 700 "${dataDir}"
fi
❝注意, shell里面的条件判断与C语言里面的不太一样, 这里我做一个解释, -d代表的就是 directory, 也就是文件夹的意思, 同理 -f 就是判断文件是否存在, 也就是file的意思, !符号的意思和C语言中一样, 就是非的意思, 注意这里的空格必须得带上, 不然会出错, 这里的方括号其实是一个运算符
至于这段代码的其他知识点, 我还是简单提一下吧, ${home}代表的就是使用变量home的值, shell里面基本都是这么引用变量的, 推荐带上这个花括号
mkdir就是创建文件夹, chmod是改变权限的命令, 这个是因为后面有一个权限相关的需求需要用到的
至于shell里面if语句的规则, 相信你看一眼就能明白
❞
-
迁移数据的这个需求其实可以理解为修改了数据库, 也就是说把数据从数据库A中全部移到了数据库B中, 移动的操作不难, 用 cp
指令就可以了
function copy_dircetory()
{
st=$(cp ./*.json "$1/")
if [ "$st" == 0 ]; then
echo "Error: Copying JSON file failed!"
fi
pwd
}
解释一下这段代码中你可能存在疑惑的几个地方
-
在shell中声明函数的形式相信你看了这段代码就懂了, 值得注意的是这里的
function
关键字不是必须的 -
st=$(cp ./*.json "$1/")
这段代码需要解释一下, 在shell中, 执行一段代码并获得执行的结果的语法有两个, 一个不推荐使用, 就是两个反引号`expr`, 另外一个就是$(expr), 推荐使用后者, 这里的./*.json
就是获取当前文件夹下面所有以.json
为后缀的文件, 后面的 $1的意思就是获取输入的第一个参数, 调用这个函数的形式如下所示:copy_dircetory destDir
-
echo
就相当于printf
, 详细的用法现在没必要介绍, 说了也忘记QwQ, 至少我是这样的
-
至于改变脚本的数据文件夹的需求, 那就更简单了, cd
命令就搞定啦
function change_directory()
{
cd "$1" || return
pwd
}
❝这段代码有一个地方值得注意, 就是这里使用了错误处理, 你先自己想一想,, 看想不想得出来^_^
好啦, 揭晓谜底:
❞cd "$1" || return
, 这里的||
其实和C语言中的一样, 是短路的, 什么意思呢, 就是说, 如果前面的语句时正确的, 后面的语句就不会执行, 如果前面的语句执行失败了, 才会执行后面的那一条语句
这里如果我们还想加一点异常提示信息怎么做呢?
cd "${targetDir}" || { echo -e "cd ${targetDir} 失败!\n"; exit 1; }
怎么样, 脚本是不是还挺有趣?
-
备份数据并打包,可以指定路径,如果未指定,打包到当前目录下
先去网上搜索一下tar
打包指令的用法吧, 我是用这个指令打的包, 这个指令的参数有一点多, 不过没必要全部看, 只用知道最常见的打包, 解包需要的那几个参数就够用了
我当时是这样实现的, 代码有点辣眼睛
backupAndTar() {
echo -e "请输入你想要打包到哪个目录: \n"
read -r tarDir
if [ ! -d "${tarDir}" ]; then
mkdir "${tarDir}"
fi
cd "${tarDir}" || { echo -e "cd ${tarDir} 失败!\n"; exit 1; }
tarDir=$(pwd)
echo -e "请输入你想要打包成的文件名: \n"
read -r fileName
echo -e "正在打包!"
#打成tar包
tar -cvf "${tarDir}/${fileName}" "${backupDir}"
echo -e "打包成功!"
}
❝
echo
的-e参数就是可以在字符串中使用转义字符, 一般只用掌握这一个参数就够了❞
read
就是读取输入的命令, -r参数就是忽视转义, 避免用户输入了一个, 如果你把它解释成转义那就麻烦了, 所以推荐加上, 其实只用掌握这一个参数就够用了
其实如果这里你想整点活的话也是可以的😁, 比如你想要给打包的文件加上日期, 那么你可以这么搞
archive=backup-$(date +"%Y-%m-%d""-""%H-%M")
if [ "$#" == 1 ]; then
archive="$1"/$archive
fi
关于这里的需求已经介绍完了, 相信你已经对于shell脚本编程有一个不错的认识了, 可以试着自己实现以下相关的需求, 你会理解的更加透彻的
需求: 同时阿方索国王不希望自己手贱修改或删除掉某个文件铸成大
你的脚本也应该支持:
-
每次运行脚本时校验文件相较上次结束脚本后是否变化,如果有变化,发出警告 -
支持从备份的数据还原
关于「数据文件夹」:为了个人信息安全,所有「数据文件夹」应该是仅所有者可读写的,如果改变「数据文件夹」操作的目标文件夹不满足要求,或者在某次脚本运行时发现当前「数据文件夹」不合规,脚本应该抛出警告。
实战:
-
对于校验文件相较上次结束后是否发生变化的逻辑我是通过MD5实现的, 因为如果文件发生变化, 那么MD5编码势必会发生变化, 所以我写了如下所示的代码(当时太菜了, 写的实在辣眼睛)
check() {
#如果不存在, 那么创建一个, 存在的话就更好...
touch "${checkFile}"
#然后遍历数据文件夹, 以id为键, 文件的MD5校验码为值
#由于我定义的文件名为id.json, 故处理起来相对简单, 虽然可扩展性极差...
flist=$(ls "${dataDir}")
cd "${dataDir}" || { echo -e "cd ${dataDir} 失败!\n"; exit 1; }
for file in ${flist}
do
md5=$(md5sum "${file}")
md5Arr=( $md5 )
m=${md5Arr[0]} #m里面存储的是这个文件的md5编码
id=${md5Arr[1]} #id里面存储的是这个人的id.json
#一开始把grep命令的参数顺序搞反了...
last=$(grep "${id}" "${checkFile}")
#如果为空
if [ -z "${last}" ]; then
echo -e "==============================================\n"
echo "新增了文件${id}"
echo -e "==============================================\n"
else
lastArr=( $last )
#这里一开始把美元$搞忘了...
lastMd5=${lastArr[0]}
#如果MD5值不相等
if [ "${m}" != "${lastMd5}" ]; then
echo -e "==============================================\n"
echo "文件${id}被修改"
echo -e "==============================================\n"
fi
fi
#这里是先把这次的MD5信息记录到一个临时文件夹里面, 之后直接复制过去覆盖实现日志更新操作
echo "${md5}" >> "checkFile.log"
done
#接下来检查是否有文件被删除?, 现在lastfList里面存储的是上次日志里面的人的id
lastfList=$(awk '{print $2}' "${checkFile}")
for lastFile in ${lastfList}
do
result=$(echo "${flist}" | grep "${lastFile}")
if [[ "${result}" == "" ]]; then
echo -e "==============================================\n"
echo -e "注意!!!文件${lastFile}被删除!\n"
echo -e "==============================================\n"
fi
done
#-f参数强制覆盖
mv -f "checkFile.log" "${checkDir}"
}
这里涉及到从一个字符串按照指定的分隔符获得数组的小技巧, 那就是($str)
, 默认的分隔符是空格, 当然你也可以指定分隔符, 相关的模板是这样的:
#比如我想要以逗号作为分隔符分割字符串
OLD_IFS="$IFS"; IFS=","
#现在就获得了titles数组
titlesArr=($titles); IFS=${OLD_IFS}
上面这段代码虽然写的很丑, 但是涉及很多知识点, 比如if-else
的用法, for
的用法, 而且还出现了许多的命令, 比如awk
, grep
等, 还演示了如何遍历一个文件夹下面的所有的文件, 希望你可以通过这段垃圾代码掌握以上的知识点, 我这里不做这些知识点的过多解释, 你可以自己试着读懂, 对你的提升一定会很大的^_^
-
对于从备份的数据还原的需求
其实这个需求的求解思路并不难, 那就是从备份文件夹中寻找是否存在你想要还原的数据, 有的话, 就cp
过来
下面这段代码实现了这个需求
#从备份的数据还原的函数, 传入的参数为待还原的文件名, 默认恢复到数据文件夹中
recover() {
#$#获得所有参数的数目, 只是为了练一下case和$#才这样写的^_^
case $# in
0)
echo -e "参数错误\n"
exit "$E_BADARGS"
esac
echo -e "正在执行还原操作...\n"
#$@获得所有参数列表
flist=$(ls "${backupDir}")
for par in "$@"
do
#首先判断这个文件是否存在备份
for list in ${flist}
do
if [ "${par}" == "${list}" ]; then
#执行复制操作, 如果数据文件夹存在这个的话, 会提示是否覆盖
cp -i "${backupDir}/${list}" "${dataDir}"
echo -e "${list}还原成功!\n"
fi
done
done
echo -e "还原操作执行完成\n"
}
-
对于权限机制, 其实就是一句话, chmod 700 dir
就完事了嗷! 想要更深入的了解权限机制, 那么可以去查阅相关的资料┗|`O′|┛ 嗷~~
需求:其次,阿方索国王希望能够方便的导入和编辑个人信
所以你的脚本应该能够支持:
-
从另一个文件夹导入数据 -
从已经打包好的数据导入 -
手动输入数据
当然,导入任何数据都有可能导致和已有数据的冲突,为了简单起见,如果有冲突发生,脚本应该询问是否全部替换,导入数据不会和自己冲突,但是可能会重复。
实战:
这些需求其实就是加载数据到数据库中, 这些数据的来源可以是另一个文件夹, 另一个打包的数据文件, 或者是用户手动输入的, 其实从另一个文件夹导入很简单, 不过需要判断一下是否重复, 至于从打包的数据导入也不难, 先解包一下, 然后调用从另一个文件夹导入数据的函数就可以了,
至于手动输入稍微复杂一些, 因为你要从用户手动输入生成一个标准的json文件, 这一步就有点麻烦了, 而且有的字段有的人可能并没有, 比如家族字段, 头衔字段, 所以需要特判一下
下面我给出我从用户手动输入生成标准json文件的代码: 其实就是纯模拟json而已
saveToJson () {
echo -e "请输入你的个人信息\n"
echo -e "请输入你的姓名: \n"
#注意: 这里的-r参数代表屏蔽转义, 因为用户层面上面来讲它并不知道转义这个东西
read -r name
echo -e "请输入你的id: \n"
read -r id
echo -e "请输入你的性别: \n"
read -r sex
echo -e "请输入你的配偶, 若有多个用空格隔开: \n"
read -r couples
couplesArr=( $couples )
couplesNum=${#couplesArr[@]}
echo -e "请问您是否有家族?(y/n)\n"
read familyOption
if [ "${familyOption}" == "y" ]; then
echo -e "请输入你的家族: \n"
read -r family
fi
echo -e "请输入你的子嗣, 如果有多个用空格隔开: \n"
read -r children
childrenArr=( $children )
childrenNum=${#childrenArr[@]}
echo -e "请问您是否有头衔?(y/n)\n"
read titlesOption
if [ "${titlesOption}" == "y" ]; then
echo -e "请输入你的主头衔: \n"
read -r maintitle
echo -e "请输入你的所有头衔: \n"
read -r titles
titlesArr=( $titles )
titlesNum=${#titlesArr[@]}
fi
#接下来将这些变量按照Json的格式存进Json文件
fileName="${dataDir}/${id}.json"
touch "${fileName}"
echo -e "{" >> "$fileName"
echo -e "\t\"name\":\" ${name}\"," >> "$fileName"
echo -e "\t\"id\": ${id}," >> "$fileName"
echo -e "\t\"sex\":\" ${sex}\"," >> "$fileName"
echo -e "\t\"couples\": [\c" >> "$fileName"
for((i=0; i<couplesNum; i++)) {
if [ $i -eq $[ ${couplesNum} - 1] ]; then
echo -e "${couplesArr[$i]}\c" >> "$fileName"
else
echo -e "${couplesArr[$i]}, \c" >> "$fileName"
fi
}
echo -e "]," >> "$fileName"
if [ "${familyOption}" == "y" ]; then
echo -e "\t\"family\":\" ${family}\"," >> "$fileName"
fi
echo -e "\t\"children\": [\c" >> "$fileName"
for((i=0; i<childrenNum; i++)) {
if [ $i -eq $[ ${childrenNum} - 1 ] ]; then
echo -e "${childrenArr[$i]}\c" >> "$fileName"
else
echo -e "${childrenArr[$i]}, \c" >> "$fileName"
fi
}
if [ "${titlesOption}" == "n" ]; then
echo -e "]" >> "$fileName"
else
echo -e "]," >> "$fileName"
fi
if [ "${titlesOption}" == "y" ]; then
echo -e "\t\"maintitle\":\" ${maintitle}\"," >> "$fileName"
echo -e "\t\"titles\": [\c" >> "$fileName"
for((i=0; i<titlesNum; i++)) {
if [ $i -eq $[ ${titlesNum} - 1 ] ]; then
echo -e "\"${titlesArr[$i]}\"\c" >> "$fileName"
else
echo -e "\"${titlesArr[$i]}\", \c" >> "$fileName"
fi
echo -e "]\c"
}
fi
echo -e "}" >> "$fileName"
}
❝这段代码中我觉得值得注意的地方有这几个:
❞
shell中类似于C语言for循环的for循环的写法, 你看一下代码就知道了, 原来还可以这么玩(๑′ᴗ‵๑) 以后遇到这种乍一看比较棘手的问题不要怕, 虽然我这种纯模拟的方法很蠢, 但后来想了一下, 貌似只能这样纯模拟了, 不然还能咋生成一个json格式的文件啊?(调包除外Qwq
-
从文件夹中导入的过程我也给一下代码, 主要是为了演示一下如何从一个json文件中提取相应的字段,这个当时我还学了一会(当然也可以直接使用jq这个库, 但是我还是使用的字符串处理三剑客
importFromDir() {
#首先进行参数检查
if [ "$#" -eq 0 ]; then
echo -e "参数错误, 请传入数据文件夹的路径!\n"
return 0
fi
echo -e "开始导入...\n"
#接下来对参数中的文件夹进行检查, 看是否存在...
for par in "$@"; do
if [ ! -d "${par}" ]; then
echo "不存在文件夹${par}"
continue
fi
#获取文件列表
flist=$(ls "${par}")
#如果文件列表为空
if [ -z "${flist}" ]; then
echo -e "文件夹${par}为空"
continue
fi
for file in ${flist}; do
#目前我判断一个文件是否符合我的要求的原则是依据id字段, 只要含有我就认为它是合格的, 当然, 这样很不严谨
res=$(grep -i id "${par}/${file}") #提取出包含id的行
if [ -z "${res}" ]; then
echo -e "文件${file}导入失败!原因: 不符合要求!\n"
continue
fi
#然后用grep以及正则表达式把id字段提取出来, 因为我的文件名是以id.json命名的, 所以方便重命名
id=$(echo ${res} | sed 's/[,"]//g; s/[ ]*//g' | awk -F ":" '{print $2}') #代码解释, 这一行相对复杂一点, sed将,和"去掉, awk以:为分隔符,并输出第二个字段
#然后重命名并且复制过来
mv "${par}/${file}" "${par}/${id}.json"
cp "${par}/${id}.json" "${dataDir}"
done
done
echo -e "导入过程结束!谢谢使用!\n"
}
里面的很多变量是一些全局的变量, 包括上面的很多代码段也是如此, 不用理会那些东西, 注释已经写得很详细了, 我就不多加解释了
需求; 首先,中世纪贵族非常看重血统,所以理清家族关系相当重要,由于整理过于麻烦,所以「并非所有人的个人信息都包含家族字段」,你的脚本应当支持
-
查询某一个人属于哪个家族。(「所有孩子都跟随父亲的家族,这里不考虑入赘的情况」)如果无法得知这个人的所属家族,提示Unknown。
-
将所有的人按家族分类导出到另一个文件夹,每个家族保存为一个文件,其中每一行包含了一个家族成员的 id 和 name,需要按id从小到大排序。
例如:
hh@hh:~# cat Jimena 1 Alfonso VI 7 blabla 11 hahaha
无法得知所属家族的人另外保存为一个文件 wildman
-
阿方索国王意外的有点小八卦,所以他希望你也能统计出所有人中的私生子。为了简化问题,如果一个人的「个人信息中」的家族信息和他「应该」的家族信息(比如他父亲的家族)不同,那么我们认为这个人很有问题,
(他爸被戴绿帽),你需要将这些人同样按照每行id和name的格式保存,但是由于这些信息过于敏感,你应该先将它base64之后按行等分成10个文件后打包保存。
实战:
哈哈哈哈, 这个项目真正恶心的需求现在才开始😜
下面来剖析一下这些需求:
-
查询某一个人属于哪一个家族:
这里需要说明一下,不是所有的人都拥有家族字段的, 所以你想要查询某一个人属于哪一个家族的话, 可以采用递归的方法: 解释一下, 就是递归的查询他父亲的家族, 如果有一个没有父亲, 那么他就是第一代人, 那么就打印家族字段, 并且, aoh, 刚才看了一下数据, 貌似并没有父亲字段, 当我没说returen
我不是这样写的, 我是先预处理把所有人和对应的家族获取并放在一个文件当中, 之后直接查询这个文件就可以了, 因为后一个需求还是需要你导出哈! 这也不失为一种思路哈!
这个需求留作正在阅读这篇文章的你的练习吧!
-
私生子这个需求其实思路不难, 但是这个需求很变态... 虽然我的代码写的很丑(原谅我写这段代码时也才学shell两天), 但是用到了很多的知识点(虽然用的很糟糕), 但还是贴出来吧, 注释写的已经很清楚了, json文件的格式文章的开头贴出来了, 可以对照着json文件来看提取字段的代码.
#找出所有的私生子
greenHat() {
greenHatFile="${greenHatDir}/greenHatFile.txt"
touch "${greenHatFile}"
flist=$(ls ${dataDir})
if [ -z "${flist}" ]; then
echo -e "数据文件夹为空!\n"
return
fi
echo -e "正在统计私生子...\n"
for file in ${flist}; do
#首先获得这个人的性别, 如果是女的话就暂时不管她
sex=$(grep -i sex "${dataDir}/${file}" | sed 's/[,"]//g;s/[ ]*//g' | awk -F ":" '{print $2}')
if [ "${sex}" == "female" ]; then
continue
fi
faId=${file%.*}
faFamily=$(queryFamily ${faId})
#获得儿子信息以及家族信息
children=$(grep -i children "${dataDir}/${file}" | awk -F ":" '{print $2}' | sed 's/[,]/ /g;s/\[//g;s/\]//g' | sed 's/[ ]*/ /g')
if [ -z "${children}" ]; then
echo -e "id为${id}的人没有孩子\n"
fi
#孩子的数组拿到了
childrenArr=( ${children} )
for child in "${childrenArr[@]}"; do
childFile="${dataDir}/${child}.json"
if [ ! -f "${childFile}" ]; then
echo -e "孩子${child}不存在"
continue
fi
childFamily=$(queryFamily ${child})
if [ "${childFamily}" != "${faFamily}" ]; then
childName=$(grep -i name "${childFile}" | sed 's/[," ]//g' | awk -F ":" '{print $2}' | sed -e 's/^[ ]*//g' | sed -e 's/[ ]*$//g')
echo -e "${child} ${childName}\n" >> "${greenHatFile}"
fi
done
done
#删掉空行
cat ${greenHatFile} | sed '/^$/d' | uniq > "${greenHatFile}"
base64Dir="${greenHatDir}/base64Dir"
if [ ! -d "${base64Dir}" ]; then
mkdir "${base64Dir}"
fi
#我这里之所以这样处理是因为我发现如果直接加密一个文件的话, 所有的加密字符会连在一起, 所以就这样又遍历里一遍QwQ
base64File="${base64Dir}/base64.txt"
touch "${base64File}"
while read line; do
echo "${line}" | base64 >> "${base64File}"
done < "${greenHatFile}"
lines=$(wc -l "${base64File}" | awk -F " " '{print $1}')
line=$((${lines}/10)) ; if [ ${line} -eq 0 ]; then line=1; fi; cd ${base64Dir} #这里cd进这个目录是因为我不知道split如何指定输出到哪个目录QwQ
#代码解释, 这一行代码非常复杂, 第一条就是split指定顺序号是四位数字, 前缀是base_, 然后ls这个目录结果作为grep的参数提取含有base_行的, 进行重命名加上后缀.txt
split -l ${line} "${base64File}" -d -a 4 base_&&ls | grep base_ | xargs -n1 -i{} mv {} {}.txt
tar -cvf "${greenHatDir}/greenHatBase.tar" "${base64Dir}"
echo -e "统计成功!\n"
}
需求:封建法中继承法是所有人关心的重点,因为这直接关系到他们死后领土的归属。
这里为了简单起见,我们假设所有地方实行的都是「男性优先均分继承法」,细化来说,只有被继承者没有儿子的情况下,女儿才有继承权,否则只有儿子有继承权,之后,在有继承权的人中最大的孩子继承被继承者的「主头衔」,其余的头衔依次由之后的继承者继承,如果头衔数多于继承者数,那么剩下的头衔在除开最大的继承者之后的继承者中继续依次分配。继承时父母头衔分别继承,互不影响。
例如:
被继承者拥有头衔(title): [1, 2, 3, 4]
主头衔为 1
继承者 A B C
其中A为长子
那么1由A继承,
之后B继承2,C继承3,B继承4
不论有多少头衔,长子只会继承一个主头衔。
每个人继承到的第一个头衔被认为是他的主头衔
同样的,个人信息中关于头衔的继承也是不完善的,很多人没有titles这个字段,你的脚本需要支持
-
查询某个人最终会继承到的头衔 -
查询某个头衔最终会被谁继承到 -
将所有头衔和最终继承到它们的人导出到另一个文件,格式为每行头衔名,继承者id和继承者name。需要按id从小到大排序。
实战:
这个需求是这个项目中最恶心的需求, 关于继承那里我想了一天, 后来感觉想不出来, 后来问了一下出题者, 发现题目确实有几个地方没有说清楚, 比如:
# 数据保证只有没有父母的人有头衔
# 主头衔继承父亲
# 私生子和正常孩子没有区别
# children第一个就是长子
我一开始这一题卡住的原因是: 我当时想到, 我想要得到一个人的头衔, 那么我需要他父母的头衔, 可是父母不一定有头衔字段啊, 又需要父亲的父母, 母亲的父母的头衔, 而且性别歧视导致这一题更难了, 这不好搞啊! 后来终于想通了, 有了一个比较好的思路, 用循环做的!
核心: 父母头衔分别继承! 由于我是按照id从小到大遍历的, 所以遍历到孩子之前, 一定会先遍历孩子的父母, 同理, 遍历孩子的父母之前, 一定会先遍历孩子的父母的父母, 由传递性可知, 遍历到孩子时, 他父母的头衔一定确定了下来!
if 遍历到男性; then
看看有没有头衔字段, 没有的话去头衔文件看看这个人有没有头衔
看有无子女;
有子女的话看有没有儿子, 如果没有儿子, 那么就传给女儿喽, 也分长女的
如果有儿子, 那么主头衔传给长子
如果查询主头衔文件, 发现儿子已经有主头衔了(那么说明长子的母亲先被遍历, 而且母亲有头衔, 于是传给了长子), 那么替换掉长子的主头衔
其他头衔分给其他儿子
else 遍历到女性
同上, 只不过如果主头衔继承给长子时, 如果主头衔文件中长子已经继承了父亲的, 那么这个主头衔将不继承给任何人(这是根据题目意思
上面这个思路理解起来可能比较困难, 我感觉很难说清楚, 代码的话太长了, 大概三百多行, 就不贴了
需求:最后,需要交互式的界面。
实战:
交互式的界面没什么好说的: 下面就是一个简陋的交互式界面
#!/bin/bash
# shellcheck source=/dev/null
#@description: 这是一个启动脚本, 在这个里面实现交互式的逻辑
#这是存放脚本的路径, 一定要使用绝对路径, 如果换了一台机器使用此脚本记得要更改路径为你的机器上的脚本文件夹所在路径
scripts="/home/wang/文档/春令营/unique2020shellProject/scripts"
#下面是一系列的初始化文件夹, 值得一提的是, 启动文件可以跟随一个参数, 这个参数就是你指定的数据文件夹哟
#脚本执行时需要接受一个参数, 就是需要操作的数据文件夹的目录, 如果不指定, 那么就是默认脚本所在得目录, 如果指定了多个, 那么抛出异常
if [ "$#" -eq 0 ]; then
home=$(pwd) #如果没有传参, 默认当前目录为主目录
elif [ "$#" -eq 1 ]; then
#先检查是否存在这个文件夹
if [ ! -d "$1" ]; then
mkdir "$1"
fi
#||后面接的是cd失败后的错误处理代码, 增强了代码的鲁棒性
cd "$1" || { echo "cd $1 失败"; exit 1;}
#避免传的是当前所在位置的一个相对路径
home=$(pwd)
else
#如果传了多个参数, 出现脑裂, 到底用哪个数据库??? 故抛出异常
echo "参数传递错误"
exit 1;
fi
#加载脚本文件
source "${scripts}/data.sh"
source "${scripts}/family.sh"
source "${scripts}/save.sh"
option=1
#下面开始交互式界面的编写
while [ ${option} != "0" -a ${option} -gt "0" -a ${option} -le "15" ]
do
clear
echo -e "============================================================"
echo -e "欢迎使用阿方索国人口管理系统!\n"
echo -e "============================================================"
echo -e "请输入你想使用的功能前面的序号使用对应的功能!\n"
echo -e " 1. 迁移数据到另一个文件夹"
echo -e " 2. 备份数据"
echo -e " 3. 打包数据"
echo -e " 4. 还原数据"
echo -e " 5. 手动输入数据"
echo -e " 6. 从另一个文件夹导入数据"
echo -e " 7. 从已经打包好的数据中导入数据"
echo -e " 8. 删除文件"
echo -e " 9. 查询某人属于哪一个家族"
echo -e " 10. 导出家族"
echo -e " 11. 统计出所有人中的私生子"
echo -e " 12. 导出头衔"
echo -e " 13. 导出所有头衔和最终继承者"
echo -e " 14. 查询某个人最终会继承到的头衔"
echo -e " 15. 查询某个头衔最终会被谁继承到"
echo -e " other. exit"
read -r option
case ${option} in
1)
clear
moveDataToAnotherDir
;;
2)
clear
echo -e "请输入你想要备份的数据\n"
read -r args
backupData ${args}
;;
3)
clear
backupAndTar
;;
4)
clear
echo -e "请输入你想要还原的数据\n"
read -r args
recover ${args}
;;
5)
clear
saveToJson
;;
6)
clear
echo -e "请输入你想要导入数据的文件夹\n"
read -r args
importFromDir "${args}"
;;
7)
clear
echo -e "请输入你想要导入的tar包\n"
read -r args
importFromTar ${args}
;;
8)
clear
echo -e "请输入你想要删除的文件\n"
read -r args
removeFile "${args}"
;;
9)
clear
echo -e "请输入你想要查询的人名"
read -r args
queryFamily "${args}"
;;
10)
clear
exportFamily
;;
11)
clear
greenHat
;;
12)
clear
exportTitle
;;
13)
clear
exportFinallySuccessor
;;
14)
clear
echo -e "请输入你想要查询的人的id:"
read -r args
queryPeopleTitle "${args}"
;;
15)
clear
echo -e "请输入你想要查询的头衔:"
read -r args
queryFinallySuccessor "${args}"
;;
esac
check
printf "\nEnter any key to go back...";
read -r a
done
福利: 我在学习shell的过程中积累的一些小trick(最开始记录了一些, 后来懒了, 就没有记录了QwQ
-
「关于如何用一个脚本创建多个文件」
i=1; while [ $i -le 99 ]; do name=`printf "test%02d.txt" $i`; touch "$name"; i=$(($i+1)); done
for i in $(seq 99); do name=$(printf test%02d.txt $i); touch "$name"; done
-
「关于判断字符串是否相等的坑」
if [ $str1 = $str2 ]
-
if和[ ]之间要空格。
-
[ ]和“ ”之间要空格
-
“ ”和=之间要空格,
-
关于如何打包
tar -cvf 目标目录全文件名 需要打包的目录
-
如何将pwd的结果保存到一个变量当中?
now=$(pwd) #这样就可以获得绝对路径了
「更好用的获取当前执行脚本的路径的方法为:」 然而我看不懂
CURDIR=$(cd $(dirname ${BASH_SOURCE[0]}); pwd )
echo $CURDIR
-
如何判断一个目录是否存在?
if [ ! -d "$backupDir" ]; then
mkdir "$backupDir"
fi
-
如何遍历一个文件夹中的所有文件?
flist=`ls ${backupDir}`
for file in $flist
do...
-
如何判断一个字符串是否为空
判断字符串为空的方法有三种:
if [ "$str" = "" ]
if [ x"$str" = x ]
if [ -z "$str" ] (-n 为非空)
注意:都要代双引号,否则有些命令会报错,养成好习惯吧!
-
获取参数的个数
$#
, 获取参数列表$@
-
将一个字符串切割 成多个元素存储到数组里面
arr=( ${str} ) #注意, 这里不能加引号, 然后空格其实可有可无
-
cd的错误检查
#为了避免cd出错, 可以加入错误检查
cd ${Dir} || { echo -e "cd ${Dir}错误"; exit 1; }
-
关于提取目的字符串的一个小技巧
sed 's/ /\n/g' example.log
❝解释下命令,s是替换的标记,第一个/ /里面有一个空格,意思是查找所有含有空格的行,最后的g表明要对该行的所有空格进行查询,而不只是查询到第一个就查询下一行,第二个/\n/是一个换行符,结合前面的空格查询语法,可以对所有的空格替换成换行符。里面的命令执行后会把文本重新编排,遇到空格就换行,这样,「目标字符串就已经到了单独的一行里面去了!!!」
❞
-
关于替换字符, 举一个例子, 替换一个或者多个空格为逗号
cat word.txt | sed 's/[ ][ ]*/,/g'
脚本说明:
-
s
代表替换指令; -
每个 [ ]
都包含有一个空格; -
*
号代表0个或多个; -
g
代表替换每行的所有匹配;
#还有一种方式,但该方式在mac下替换失败:
cat word.txt | sed 's/\s\+/,/g'其中\s代表空格,+代表出现一次或多次。
再说一个我的实战吧, 我需要提取json文件中键为id的字段的值
id=$(grep -i id ${res} | sed 's/[,"]//g' | awk -F ":" '{print $2}')
-
在shell中 以及
$()
是用作命令替换的${}
是用作变量替换的, -
我现在需要处理一个打包的文件, 所以我需要获得它的后缀来判断是什么类型的, 方便我进行相应的处理
${file##*.} #代码解释, 删掉最后一个.及其左边的字符串, 也就是说只保留最后的类型
可以用${ }分别替换得到不同的值:
${file#*/}:删掉第一个 / 及其左边的字符串:dir1/dir2/dir3/my.file.txt
${file##*/}:删掉最后一个 / 及其左边的字符串:my.file.txt
${file#*.}:删掉第一个 . 及其左边的字符串:file.txt
${file##*.}:删掉最后一个 . 及其左边的字符串:txt
${file%/*}:删掉最后一个 / 及其右边的字符串:/dir1/dir2/dir3
${file%%/*}:删掉第一个 / 及其右边的字符串:(空值)
${file%.*}:删掉最后一个 . 及其右边的字符串:/dir1/dir2/dir3/my.file
${file%%.*}:删掉第一个 . 及其右边的字符串:/dir1/dir2/dir3/my
记忆的方法为:
# 是 去掉左边(键盘上#在 $ 的左边)
%是去掉右边(键盘上% 在$ 的右边)
单一符号是最小匹配;两个符号是最大匹配
${file:0:5}:提取最左边的 5 个字节:/dir1
${file:5:5}:提取第 5 个字节右边的连续5个字节:/dir2
也可以对变量值里的字符串作替换:
${file/dir/path}:将第一个dir 替换为path:/path1/dir2/dir3/my.file.txt
${file//dir/path}:将全部dir 替换为 path:/path1/path2/path3/my.file.txt
-
sort将文件按照某一指定的字段来进行排序操作
sort test1 | uniq #删除重复行
16 删除纯空行和由空格组成的空行
sed '/^[ ]*$/d' file
-
删除字符串前后的空格
sed -e 's/^[ ]*//g' | sed -e 's/[ ]*$//g'
-
删除空行和重复行(连续运行几次, 输出的都不一样也是真的诡异)
cat ${greenHatFile} | sed '/^$/d' | uniq > ${greenHatFile}
-
读取文件的每一行
while read line
do
echo $line
done < file#(待读取的文件)
-
给split分割后的文件加上后缀
今天又用到了split命令,想批量给分割后的文件添加扩展名,终于找到一个靠谱的方法,记录如下:
split kws.txt -l 1000 -d -a 2 url_&&ls|grep kws_|xargs -n1 -i{} mv {} {}.txt
❝解释一下,将kws.txt文件按每个文件1000行分割,分割后的文件命名为kws_00....kws_01....kws_02等,等split命令执行完了,紧接着执行第二条命令ls|grep kws_|xargs -n1 -i{} mv {} {}.txt,意思是先查找kws_开头的文件,然后逐个重命名为.txt
❞
split -l 2000 urls.txt -d -a 2 url_
❝解释一下:
-l:按行分割,上面表示将urls.txt文件按2000行一个文件分割为多个文件
-d:添加数字后缀,比如上图中的00,01,02
-a 2:表示用两位数据来顺序命名
url_:看上图就应该明白了,用来定义分割后的文件名前面的部分。
❞
-
数组处理的骚操作, 有了这个, 我前面一个不知道怎么处理的错误现在知道怎么改了
array=(bill chen bai hu);
num=${#array[@]}
for ((i=0;i<num;i++)){
echo $array[i];
}
❝获取数组的所有元素:
❞
${array[*]}
遍历数组就编程了非常简单的事情:
for var in ${array[*]}
do
echo $var;
done
获取数组某一个单元的长度就变成了:
${#var}
-
获取字符串所在行, 只输出行号
今天遇到了一个问题, 就是我想要在源文件的某一行末尾加上一些内容, 那么就得先获取源文件的这一行然后sed -i, 所以现在讲讲如何获取某一行的行号
grep可以输出行号, 但同时也会输出内容
# 输出内容同时输出行号
grep -n "要匹配的字符串" 文件名
awk可以只输出行号, 而不输出内容
# 输出行号,并不输出内容
# 注意是单引号
awk '/要匹配字符串/{print NR}' 文件名
这里涉及一个小的知识点,「如何在awk中写变量」呢。 比如“要匹配字符串”位置想要写入一个变量,要在变量外加单引号,再加双引号:
# 变量赋值
word=hello
# 在awk中引入变量, 打印变量所在的行
awk '/"'${word}'"/{print $0}' file.txt
# 其中${word}是变量较好的写法,$word 的写法也可以执行
但是我在awk里面套了正则之后好像提取不到结果了, 于是我用的grep加上awk提取第一个字段获得行号
-
知道了行号, 如何在源文件中的某一行进行修改?
sed -i -e "${line}s/$/,${titlesArr[$i]}/" ${titlesFile}
❝解释一下: -i代表在源文件里面修改, -e貌似是多条命令,
❞代表匹配末尾, 再后面的表示替换末尾为这个字符串, 也就是说在末尾追加这个, 再后面跟的就是源文件
-
sed替换整行
sed -i -e "${line}s/^.*$/,${mainTitle}/" "${titlesFile}"
-
如何逐行遍历一个文件
-
通过read命令, read读取文件时, 每次调用会读取文件中的一行文本
cat file.txt | while read line
do
echo ${line}
done
while read line; do
echo ${line}
done < file.txt
-
使用for var in line处理
for line in $(cat file.txt); do
echo ${line}
done
完结撒花✿✿ヽ(°▽°)ノ✿
原创不易!如果觉得这篇文章帮助到你了的话, 那就点个赞, 点个关注呗!
本文使用 mdnice 排版