[机派X] Day 9 - 玩转 Bash:原来 Bash 还有这些妙用

引言

今天是机派X系列文章的第九天。
昨天介绍了 Linux 的档案系统与 Bash 的实际操作,大家都熟悉了吗?
今天要应用昨天介绍的 bash 指令写一些小程序,希望透过这些小程序来熟悉指令的用法。

本篇大纲:

  • 引言
  • Example1 - 跟大家说早安
  • Example2 - 建立专案结构
  • Example3 - 备份档案并加入时间戳记
  • Example4 - 依日期分类档案
  • Example5 - 依据环境自动连接 Wi-Fi
  • 结语
  • 关於本文章系列

也许刚入手指令,所以你会觉得图形化使用者界面比较方便,但是其实指令熟悉後会发现好处多多。举例来说,有些简单又具备重复性质的工作,可以透过几行指令轻松完成,如此一来便能节省许多时间。反观,如果为了一些琐碎的小事便要大动干戈,撰写图形化使用者界面的程序其实是很耗时且不切实际的。

虽然市面上已有许多别人开发好的免费工具可以使用,但是别人开发的工具也许只满足别人的需求,却无法满足你我的需求。尤其当我们越向更深的领域探索,所需要的工具客制化程度也会越高,别人的工具可能将会无法满足我们的需求。

也许 python 等主打轻松入门的程序语言可以让你轻松撰写程序,并完成高度客制化的需求,然而这些程序语言本身却也存在一些缺点。以 python 来说,python 的多用途奠基在丰富的函式库(Library)上,往往许多功能都需要另外安装函式库後才能使用,函式库的安装与维护又是另外一门学问。

透过 bash 便能克服以上的痛点,由於 bash 本身内装於系统中,相关的指令也都是作业系统的一部份,因此使用上无须额外安装或设定,与系统间的整合也会更紧密与稳定。当然,这并不表示你应该舍弃其他程序语言,并专情於 bash ,因为每个程序语言或工具都有其诞生的目的,面对特定需求采用最适合的工具才是我们所追求的。希望在以下几个范例中,能够让你熟悉常用的指令,并且发掘这些指令能够为你提供哪些实质上的帮助。

Example1 - 跟大家说早安

我们可以将多条 Bash 指令包在一个或多个档案中执行,而含有 Bash 指令的档案称为程序脚本(Script)。

举例来说,我每天要跟 Kevin、Tina 还有 Mary 分别说早安,我可以这样下指令:

echo '早安,Kevin'
echo '今天很漂亮喔,Tina !!'
echo '早上好,Mary'

或者:

echo -e '早安,Kevin\n今天很漂亮喔,Tina !!\n早上好,Mary'

每次都要输入那麽多字是不是很麻烦呢?
将以上内容存成档案 greet ,之後就可以直接呼叫 greet 来「打招呼」了!

透过以下指令,可以将第二种写法写入至档案中。

echo 'echo -e '\''早安,Kevin\n今天很漂亮喔,Tina !!\n早上好,Mary'\' > greet

接着,你可以透过 cat 来确认 greet 当中的内容:

cat greet

要执行(Execute)某个档案前,需要先赋予它执行权限。
(如果这条指令让你觉得迷惑,请参考 Day 5 的文章。)

chmod u+x greet

之後只要直接输入档案的路径与档名就可以直接执行该档案了!

/home/ubuntu/greet

也可以使用相对路径来操作:

cd ~/happy
../greet

不过你可能会发现一件诡异的事:

cd ~
greet

会报错:

bash: greet: command not found

会出现这样的状况在於 Bash 预设没有将当前的目录加入执行档的搜寻路径中。Bash 本身会纪录「哪些路径中有执行档」,并储存於 PATH 这个变数中。当我们下了一道指令,例如:ls ,Bash 就会在 PATH 里面的每条路径中找寻,是否有个档案具备执行权限,且档案名称是 ls ,若有找到就会直接执行,如果找遍所有 PATH 中的路径都没有符合的项目,Bash 就会报错说:该指令(command , CMD)不存在。

在预设状态下,当前目录(也就是 .)并未在 PATH 中,因此在 greet 所在目录直接输入 greet 会使 Bash 找不到当前目录下的 greet 档案来执行,从而导致错误发生。

要解决这个问题,有两种方法:

  1. 将当前目录 . 加入至 PATH 中。
  2. 直接给予明确的档案位置 ./greet 。

如果要采用第一种方法,就要变更 PATH 变数中的内容,将当前路径(也就是 .)加到 PATH 中,由於 PATH 中,不同路径间是以 : 隔开的,因此可以这样下指令:

export PATH="$PATH:."
cd ~
greet

如果要采用第二种方法,可以直接这样下指令。

cd ~
./greet

至此,你已经成功写出一个最简单的脚本了!

如果觉得每次写脚本都要辛苦的用 echo 跟 > 来做很麻烦的话,可以改用 vi 、vim 或 nano 等文字编辑器喔!
也可以搭配之前介绍过的注解,让你的程序内容更加完整。

#####################################################
#  greet                                            #
#    用来跟同事打招呼用的脚本,会在萤幕上印出打招呼的讯息。  #
#    虽然看起来很废,但是我深信这是学习脚本的一小步。       #
#                                                   #
#                                      by haward79  #
#                                       2021 09/10  #
#####################################################

# 跟同事打招呼
echo '早安,Kevin'
echo '今天很漂亮喔,Tina !!'
echo '早上好,Mary'

Example2 - 建立专案结构

每次手动建立专案是否觉得很麻烦呢?
这个范例让我们写一个小脚本,让它在当前工作目录自动建立一个专案,并初始化 git。

echo "Create project in '$(pwd)' "
read -p 'Press ENTER to continue ......' trash

echo ''
read -p 'Please input name of the project: ' name

if [[ -n $name ]]
then
    if [[ ! -d "$name" ]]
    then
        mkdir "$name"
        cd "$name"

        mkdir src bin
        
        echo -e '#include <iostream>\n\nusing std::cout;\nusing std::cin;\n\nint main()\n{\n	cout << "Hello World.\n";\n\n	return 0;\n}\n\n' > 'src/main.cpp'

        echo '.gitignore' > .gitignore

        git init
        git add .
        git commit -m 'First commit: project structure created'

        echo 'Done.'
    else
        echo "Project '$name' already exists."
    fi
else
    echo 'Please input a valid project name.'
fi

Example3 - 备份档案并加入时间戳记

修改设定档时,有时候会需要先备份档案,以防档案改坏时发生悲剧、无法还原。
备份的档案有时也会加上时间戳记,以认明这是何时修改的。

以下脚本能够备份档案,并在档案的结尾自动补上当前的时间。
为了让这个脚本能够被其他脚本呼叫、再利用,因此我的设计是它也能透过引数读取要备份档案的档名,而未必要与使用者互动输入档名。

if [[ -n $1 ]]
then
    filename="$1"
else
    read -p 'Please input filename: ' filename
fi

if [[ -f $filename ]]
then
    cp "$filename" "$filename.bak"
    NOW=$(date +'%Y%m%d %H%M%S')
    echo -e "\nThis file is backed up by a SMART SCRIPT.\nBackup created at $NOW" >> "$filename.bak"
else
    echo "File: '$filename' NOT found."
fi

Example4 - 依日期分类档案

某天手机的容量爆满通知,终於迫使你整理手机中的照片与影片,然而将档案从手机转移到电脑後,望着大量档案却不知从何下手整理,因为档案命名都是以日期和时间组合而成,照片与影片的命名字首也不相同,此时就让我们写个简单的脚本来处理这个问题吧!
以我自己的手机为例,照片命名格式为:IMG_年月日_时分秒.jpg ,影片命名格式为:VID_年月日_时分秒.mp4 。我们将写一个脚本,用於将一个混杂照片与影片的资料夹整理成数个以日期为索引的资料夹,每个资料夹中的档案都只留时间作为档名。

例如,工作目录中含有原始档案:

  • Camera/
    • IMG_20200901_080005.jpg
    • IMG_20200901_080343.jpg
    • VID_20200902_070213.mp4
    • IMG_20200903_132712.jpg
    • IMG_20201018_123000.jpg
    • IMG_20201018_135214.jpg
    • IMG_20201018_200403.jpg
    • VID_20201018_000215.mp4
    • VID_20201018_001826.mp4

我们希望整理成以下结构:

  • Sorted/
    • 20200901
      • 080005.jpg
      • 080343.jpg
    • 20200902
      • 070213.mp4
    • 20200903
      • 132712.jpg
    • 20201018
      • 123000.jpg
      • 135214.jpg
      • 200403.jpg
      • 000215.mp4
      • 001826.mp4
if [[ -d 'Sorted' ]]
then
    rm -r 'Sorted'
fi

mkdir 'Sorted'

for file in Camera/*
do
    export file_ext=$(echo "$file" | rev | cut -d . -f 1 | rev)
    export file_revWithoutExt=$(echo "$file" | rev | cut -d . -f 2)
    export file_date=$(echo "$file_revWithoutExt" | cut -d _ -f 2 | rev)
    export file_time=$(echo "$file_revWithoutExt" | cut -d _ -f 1 | rev)

    echo "Origin : $file"
    echo "Date   : $file_date"
    echo "Time   : $file_time"
    echo "Ext    : $file_ext"
    echo '----------------------------------------'

    if [[ ! -d "Sorted/$file_date" ]]
    then
        mkdir "Sorted/$file_date"
    fi

    mv "$file" "Sorted/$file_date/$file_time.$file_ext"
done

Example5 - 依据环境自动连接 Wi-Fi

我自己使用树莓派时,通常都是藉由电脑远端连线,这样可以免去转换工作环境时还要重新差拔树莓派的周边设备。
也是因此,每次使用树莓派,我都是直接接电源线而已!!

不过这会面临一个问题:我希望树莓派能随着环境的变化自动切换要连接的无线网路。
例如:我在家里使用时,树莓派要连接到家里的 Wi-Fi ;当我在学校使用时,则连接到手机的 Wi-Fi。
这样才是真正的免萤幕、免键盘啊!只要树莓派跟电源线带着走,到哪里都可以立刻当码奴!

这就是以下这个脚本诞生的由来!
执行这个脚本之前要先设定 home-wifi 与 mobile-wifi 连线,包含 Wi-Fi 的 SSID、密码等资讯。
之後,只需要将这个脚本 利用 rc-local 服务设定成开机自动执行 即可。
每次开机後,以下脚本就会自动执行,并依照能抓到的 Wi-Fi SSID 自动完成连线。

for(( ; 1; ));
do
	if [[ $(nmcli device show wlan0 | grep 'home-wifi') = '' && $(nmcli d wifi list | grep 'home-wifi') != '' ]]
    then
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Wi-Fi: home-wifi."
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Clear other connection ..."
		nmcli dev disconnect wlan0
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Connecting to home-wifi ..."
		nmcli con up home-wifi
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
	elif [[ $(nmcli device show wlan0 | grep 'home-wifi') = '' ]] && [[ $(nmcli d wifi list | grep 'mobile-wifi') != '' ]] && [[ $(nmcli device show wlan0 | grep 'mobile-wifi') = '' ]]
    then
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Wi-Fi: mobile-wifi."
    	echo "$(date +'%Y.%m.%d %H:%M:%S') | Clear other connection ..."
		nmcli dev disconnect wlan0
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Connecting to mobile-wifi ..."
		nmcli con up mobile-wifi
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
	else
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
		echo "$(date +'%Y.%m.%d %H:%M:%S') | No operation to do." >> 'keep_wifi_connection.log';
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
	fi
	
	sleep 5
done

结语

以上就是五个简单的 Bash 指令范例,范例中为求简单易懂,因此仍有许多的状况未列入脚本设计时的考量。
如果以上脚本成功引起你对 Bash 的兴趣,也欢迎你把这些脚本改良或客制化,成为你最实用的小工具。

关於本文章系列

如果对於文章内容有建议、纠错或图源标示不正确的问题,欢迎参考 [机派X] Day 1 尝试与文章作者联络。
想看更多本系列的文章,请连结至 [机派X] Day 1 查看大纲。


<<:  虹语岚访仲夏夜-7(专业的小四篇)

>>:  [Day20]C# 鸡础观念- 物件导向(oop)基本观念

Day28-保护鲸鱼人人有责(三)

小结 前两天讲了几个比较重要的 Dockerfile best practice 之後,今天要来说一...

在本机浏览Open API格式的文件

原文: https://j2hongming.github.io/2021/08/06/view-o...

[Day 7] 从零开始的股票预测 - 异常值侦测

一、异常值(Outliers) 异常值是指某些大幅度偏离正常值的资料点,来源可能是测量异常或记录异常...

Day29练习java-多执行序

昨天是用继承Thread来执行多执行序,今天介绍另外一种方法,实作Runnable介面一样可以执行多...

常用工具介绍(1)-postman、ngrok

今天先来介绍我们之後会使用到的工具, 在本地开发的困扰就是, 别人的机器、服务器连不到你, 这时候n...