主題 2 Shell工具和指令碼

2023-01-04 06:00:49

主題 2 Shell工具和指令碼

Shell 工具和指令碼 · the missing semester of your cs education (missing-semester-cn.github.io)

Shell指令碼

shell 指令碼是一種更加複雜度的工具。

  • 定義變數

在bash中為變數賦值的語法是foo=bar,意為定義變數foo,foo的值為bar。存取變數使用$變數名

[lighthouse@VM-8-17-centos tools]$ foo=bar
[lighthouse@VM-8-17-centos tools]$ echo "$foo"
bar

需要注意的是,Shell中使用空格作為分隔引數的保留字元。

如果將上訴賦值語句寫為foo = bar,將不起作用。事實上,這樣寫並沒有將bar賦給foo,而是用=bar作為引數呼叫foo程式。因為這樣Shell會認為你正在執行一個名為foo的命令。

[lighthouse@VM-8-17-centos tools]$ foo = bar
-bash: foo: command not found

你需要特別注意這類問題,比如如果有帶空格的檔名,你需要使用引號將其括起來。

  • 在bash中處理字串

有兩種定義字串的方法,可以使用雙引號定義字串,也可以使用單引號定義字串。

[lighthouse@VM-8-17-centos tools]$ echo "Hello"
Hello
[lighthouse@VM-8-17-centos tools]$ echo 'Hello'
Hello

Bash中的字串通過'"分隔符來定義,但是它們的含義並不相同。

'定義的字串為原義字串,其中的變數不會被跳脫,而 "定義的字串會將變數值進行替換。

例如:

[lighthouse@VM-8-17-centos tools]$ echo "Value is $foo"
Value is bar
[lighthouse@VM-8-17-centos tools]$ echo 'Value is $foo'
Value is $foo
  • 定義函數

和其他大多數的程式語言一樣,bash也支援if, case, whilefor 這些控制流關鍵字。同樣地, bash 也支援函數,它可以接受引數並基於引數進行操作。

下面這個函數是一個例子,它會建立一個資料夾並使用cd進入該資料夾。

[lighthouse@VM-8-17-centos tools]$ cat mcd.sh
mcd(){
	mkdir -p "$1"
	cd "$1"
}

這裡 $1 是指令碼的第一個引數的意思

source 指令碼名,這將會在Shell中載入指令碼並執行。

[lighthouse@VM-8-17-centos tools]$ source mcd.sh
[lighthouse@VM-8-17-centos tools]$ mcd test
[lighthouse@VM-8-17-centos test]$ 

如上,在執行了source mcd.sh之後,看似無事發生,但實際上Shel中已經定義了mcd函數。我們給mcd傳遞一個引數test,這個引數被用於作為建立的目錄名(即$1),然後Shell自動切換到了test目錄裡。整個過程就是,我們建立了資料夾並進入其中。

  • 保留字

在bash中,許多$開頭的東西一般都是被保留的(指留作特定用途)

$1 是指令碼的第一個引數的意思。與其他指令碼語言不同的是,bash使用了很多特殊的變數來表示引數、錯誤程式碼和相關變數。下面列舉其中一些變數,更完整的列表可以參考 這裡

形式 釋義
$0 指令碼名
$1 ~ $9 指令碼的引數, $1 是第一個引數,依此類推
$@ 所有引數
$# 引數個數
$? 前一個命令的返回值
$$ 當前指令碼的程序識別碼
!! 完整的上一條命令,包括引數。常見應用:當你因為許可權不足執行命令失敗時,可以使用 sudo !!再嘗試一次。
$_ 上一條命令的最後一個引數,如果你正在使用的是互動式 shell,你可以通過按下 Esc 之後鍵入 . 來獲取這個值。

有一些保留字可以直接在Shell中使用,例如$?可以獲取上一條命令的錯誤程式碼(返回值),再比如$_會返回上一條命令的最後一個引數。

例如:

[lighthouse@VM-8-17-centos tools]$ mkdir test
[lighthouse@VM-8-17-centos tools]$ cd $_
[lighthouse@VM-8-17-centos test]$ 

如上,我們無需在寫一次test,使用$_存取該引數,它就會被替換成test,現在我們進入到test目錄中了。

這樣的例子有很多,再例如!!,它返回完整的上一條命令,包括引數。常見應用:當你因為許可權不足執行命令失敗時,可以使用 sudo !!再嘗試一次。

[lighthouse@VM-8-17-centos tools]$ mkdir /mnt/new
mkdir: cannot create directory ‘/mnt/new’: Permission denied
[lighthouse@VM-8-17-centos tools]$ sudo !!
sudo mkdir /mnt/new
[lighthouse@VM-8-17-centos tools]$ rmdir /mnt/new
rmdir: failed to remove '/mnt/new': Permission denied
[lighthouse@VM-8-17-centos tools]$ sudo !!
sudo rmdir /mnt/new
[lighthouse@VM-8-17-centos tools]$ 
  • 標準錯誤流

如果你的程式出錯了,你想輸出錯誤但不想汙染標準輸出,那麼你可以寫進這個流。

  • 錯誤程式碼

還有一種叫做錯誤程式碼$?(error code)的東西,是一種告訴你整個執行過程結果如何的方式。

[lighthouse@VM-8-17-centos tools]$ echo "Hello"
Hello
[lighthouse@VM-8-17-centos tools]$ echo $?
0

這裡顯示echo "Hello" 執行的錯誤程式碼為0,0是因為一切正常,沒有出現問題。

這種退出碼和如C語言裡代表的意思一樣。

0代表一切正常,沒有出現錯誤。

[lighthouse@VM-8-17-centos tools]$ grep foobar mcd.sh
[lighthouse@VM-8-17-centos tools]$ echo $?
1

如上,我們嘗試著在mcd.sh指令碼中查詢foobar字串,而它不存在,所以grep什麼都沒輸出。但是通過反饋一個1的錯誤程式碼,它讓我們知道這件事沒有成功。

此外,true的錯誤程式碼始終是0;false的錯誤程式碼則是1。

[lighthouse@VM-8-17-centos tools]$ true
[lighthouse@VM-8-17-centos tools]$ echo $?
0
[lighthouse@VM-8-17-centos tools]$ false
[lighthouse@VM-8-17-centos tools]$ echo $?
1
  • 邏輯運運算元

下面bash要做的是執行第一個命令,如果第一個命令失敗,再去執行第二個(短路運演演算法則)。因為它嘗試做一個邏輯或,如果第一個命令沒有0錯誤碼,就會去執行第二個命令

[lighthouse@VM-8-17-centos tools]$ false || echo "Oops fail"
Oops fail

相似地,如果我們把false換成true,那麼將不會執行第二個命令,因為第一個命令已經返回一個0錯誤碼了,第二個命令將會被短路。

[lighthouse@VM-8-17-centos tools]$ true || echo "Oops fail"
[lighthouse@VM-8-17-centos tools]$ 

相似的,我們使用與運運算元&&,它僅當第一個命令執行無錯誤時,才會執行第二個部分。如果第一個命令失敗,那麼第二個命令就不會被執行。

[lighthouse@VM-8-17-centos tools]$ true && echo "Things went well"
Things went well
[lighthouse@VM-8-17-centos tools]$ false && echo "This will not print"
[lighthouse@VM-8-17-centos tools]$ 

使用;號連線的程式碼,無論你執行什麼,都可以通過。在同一行使用分號來連線命令,如下,它始終會被列印出來。

[lighthouse@VM-8-17-centos tools]$ false ; echo "This will always print"
This will always print
  • 把命令的輸出存到變數裡

這裡我們獲取pwd命令的輸出,它會列印出我們當前的工作路徑,然後把其存入foo變數中。然後我們詢問變數foo的值,我們就可以看到這個字串

[lighthouse@VM-8-17-centos tools]$ foo=$(pwd)
[lighthouse@VM-8-17-centos tools]$ echo $foo
/home/lighthouse/missing-semester/tools

更廣泛地來說,我們可以通過一個叫做命令替換的東西,把它放進任意字串中。並且因為我們使用的不是單引號,所以這串東西會被展開。

[lighthouse@VM-8-17-centos tools]$ echo "We are in $(pwd)"
We are in /home/lighthouse/missing-semester/tools
  • 過程替換

另一個比較好用知名度更低的東西叫做過程替換。和之前的命令替換是類似的,例如

[lighthouse@VM-8-17-centos tools]$ cat <(ls) <(ls ..)
mcd.sh
test
tools

如上,<(ls) <(ls ..)的作用是,()內部的命令會被執行,其輸出將被儲存到一個臨時檔案內,然後把檔案的識別符號handle交給最左邊的命令。

因此,這裡我們在ls這個目錄,把輸出放到臨時檔案內,再對父目錄如法炮製,然後把兩個檔案連線。

這種寫法非常方便,因為有些命令會從某個檔案的內容,而不是從標準輸入裡,獲得輸入引數

綜合案例:

現在來看一個裡面包含這些內容的簡單範例指令碼:

example.sh

#!/bin/bash
  
echo "Start program at $(date)" # Date will be substituted

echo "Running program $0 with $# arguments with pid $$"

for file in "$@";do
        grep foobar "$file" > /dev/null 2> /dev/null
        # When pattern is not found,grep has exit status
        # We redirect STDOUT and STDERR to a null register ..
        if [[ "$?" -ne 0 ]]; then
                echo "File $file does not have any foobar, adding one"
                echo "# foobar" >> "$file"
        fi      
done

第三行:有一個$(date)的引數,date列印出當前的時間。

第五行:$0代表著當前執行的指令碼的名稱,$#代表給定的引數個數,$$是這個命令的程序ID,一般縮寫為PID。

第七行:$@可以展開成所有引數,比如有三個引數,你可以鍵入$1 $2 $3,如果你不知道有多少個引數,也可以直接鍵入$@。這裡我們通過這種方式將所有引數放在這裡,然後這些引數被傳給for迴圈,for迴圈會建立一個file變數,依次地用這些引數賦值給file變數。

第八行:我們執行grep命令,它會在一堆檔案裡搜尋一個子串。這裡我們在檔案裡搜尋字串foobar,檔案變數file將會展開為賦給它的值。

之前說過,如果我們在意程式的輸出的話,我們可以把它重定向到某處(比如到一個檔案裡面儲存下來,或者連線組合)。但有時候情況恰恰相反,例如有時候我們只想知道某個指令碼的錯誤程式碼是什麼,例如這裡想知道grep能不能成功查詢。我們並不在意程式的執行結果,因此我們甚至能直接扔掉整個輸出,包括標準輸出和標準錯誤流。這裡我們做的就是把兩個輸出重定向到/dev/null,/dev/null是UNIX系統的一種特殊裝置,輸入到它的內容會被丟棄(就是說你可以隨意亂寫亂畫,然後所有的內容都會被丟掉)。

這裡的>代表重定向輸出流,2>代表重定向標準錯誤流(因為這兩個流是分立的,所以你要告訴bash去操作哪一個)。

所以這裡我們執行命令,去檢查檔案有沒有foobar字串,如果有的話,返回一個0錯誤程式碼,如果沒有返回一個非0錯誤程式碼。

第十一行:我們獲取前一個命令的錯誤程式碼($?),然後是一個比較運運算元-ne(代表不等於Non Equal)

其他程式設計序語言中有像=和≠,bash裡有很多預設的比較運算(可以使用命令man test檢視),這主要是為了你用Shell的時候,有很多東西要去測試。比如我們現在正在對比兩個數,看它們是否相同。

如果檔案中沒有foobar,前一個命令將會返回一個非零錯誤程式碼。

第十二行:我們將會如果前一個命令返回一個非0錯誤程式碼,我們將會輸出一句話File xxx does not have any foobar, adding one

第十三行:使用>>往對應檔案中追加一行註釋# foobar

現在我們來執行這個指令碼,當前目錄下有一些檔案,我們將這些檔案作為引數傳給example.sh,檢查是否有foobar。

[lighthouse@VM-8-17-centos tools]$ ls
example.sh  hello.txt  mcd.sh
[lighthouse@VM-8-17-centos tools]$ ./example.sh hello.txt mcd.sh
Start program at Sun Dec 25 23:06:13 CST 2022
Running program ./example.sh with 2 arguments with pid 2570038
File hello.txt does not have any foobar, adding one
File mcd.sh does not have any foobar, adding one

我們在檔案hello.txt和mcd.sh中沒有找到foobar字串,因此指令碼分別給這兩個檔案新增了一個# foobar 註釋

[lighthouse@VM-8-17-centos tools]$ cat hello.txt
hello,this is a txt file
# foobar
[lighthouse@VM-8-17-centos tools]$ cat mcd.sh 
mcd(){
	mkdir -p "$1"
	cd "$1"
}
# foobar
  • 萬用字元

如果我們不想一個一個查詢檔案,可以使用萬用字元來進行匹配。

比如這裡*匹配任意字元,這裡將會顯示出所有含有任意字元,並以.sh結尾的檔案

[lighthouse@VM-8-17-centos tools]$ ls
example.sh  hello.txt  image.png  mcd.sh  project1  project2  test
[lighthouse@VM-8-17-centos tools]$ ls *.sh
example.sh  mcd.sh

現在如果我只想找有一個而不是兩個特定字元的項,可以使用??匹配一個字元

[lighthouse@VM-8-17-centos tools]$ ls
example.sh  hello.txt  image.png  mcd.sh  project1  project2  project42  test
[lighthouse@VM-8-17-centos tools]$ ls project?
project1:
src

project2:
src

現在我們得到了匹配的目錄project1和project2

src是匹配的目錄下的子項

總而言之,萬用字元非常強大,你也可以組合它們。

一個常用模式是花括號{}

比如目錄下有一個image.png圖片,我們想轉變該影象的格式,一般的做法是convert image.png image.jpg,但是你也可以鍵入convert image.{png,jpg},它會展開成上面的那行。

又如:

[lighthouse@VM-8-17-centos tools]$ touch foo{,1,2,10}
[lighthouse@VM-8-17-centos tools]$ ls
example.sh  foo  foo1  foo10  foo2  hello.txt  mcd.sh project1  project2 test

如上所述,我們可以touch一串foo,所有的foo都會被展開。

你也可以進行多層操作,建立笛卡爾系:

[lighthouse@VM-8-17-centos tools]$ cat <(ls project?/src/test)
project1/src/test:

project2/src/test:
[lighthouse@VM-8-17-centos tools]$ touch project{1,2}/src/test/test{1,2,3}.py
[lighthouse@VM-8-17-centos tools]$ cat <(ls project?/src/test)
project1/src/test:
test1.py
test2.py
test3.py

project2/src/test:
test1.py
test2.py
test3.py

如上,我們在建立檔案的路徑上有兩組花括號,這會用兩組展開式形成笛卡爾積,意味著展開後所有的路徑有2*3組。因此當我們執行命令touch project{1,2}/src/test/test{1,2,3}.py時,實際上分別在./project1/src/test/目錄下和./project2/src/test/目錄下建立了test1.pytest2.pytest3.py檔案。

你也可以將*萬用字元和{}萬用字元結合,甚至用一些範圍表示,如

[lighthouse@VM-8-17-centos tools]$ mkdir foo bar
[lighthouse@VM-8-17-centos tools]$ touch {foo,bar}/{a..d}
[lighthouse@VM-8-17-centos tools]$ cat <(ls {foo,bar}/)
bar/:
a
b
c
d

foo/:
a
b
c
d

如上,這將會從foo/a一直到展開到foo/d,而bar目錄下同理。

  • diff

diff 命令用於比較檔案的差異。diff 以逐行的方式,比較文字檔案的異同處。如果指定要比較目錄,則 diff 會比較目錄中相同檔名的檔案,但不會比較其中子目錄。

[lighthouse@VM-8-17-centos tools]$ touch foo/x bar/y
[lighthouse@VM-8-17-centos tools]$ diff <(ls foo) <(ls bar)
5c5
< x
---
> y

如上,x只在第一個資料夾裡,而y只在第二個資料夾內。

  • 其他Shell指令碼

目前為止我們只看了bash指令碼,如果你喜歡其他指令碼(bash對一些工作可能並不是最好的選擇),你可以用很多語言寫和Shell工具互動的指令碼。注意,指令碼並不一定只有用 bash 寫才能在終端裡呼叫。比如說,這是一段 Python 指令碼,作用是將輸入的引數倒序輸出:

#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)

如上,python預設不會嘗試和Shell互動,所以我們需要匯入一些庫import sys。第一行叫做shebang,Shell通過它瞭解怎麼執行這個程式。

shebang這個單詞源於這行以#!開頭,#是sharp,!是bang

你可以隨時鍵入類似python script.py a b c的命令來執行這個python指令碼:

[lighthouse@VM-8-17-centos tools]$ python script.py a b c
c
b
a

但是如果想讓它從Shell就能執行呢?這就需要用到shebang行。Shell用首行識別到需要用Python直譯器執行這個程式,並且第一行給出了python直譯器所在的路徑。

[lighthouse@VM-8-17-centos tools]$ ./script.py a b c
c
b
a

需要注意的是不同的裝置很可能會把python放在不同的地方,最好不要假設檔案放在固定的位置,其他的東西要是如此。

shebang 行中使用 env 命令,會根據給出的引數(這裡是python),env 會利用之前的PATH 環境變數來進行定位,在此路徑中找python二進位制檔案,然後用該檔案去解釋這個指令碼。這會有更好的可移植性

#!/usr/bin/env python
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)
  • shellcheck

編寫bash指令碼有時候會很彆扭和反直覺。例如 shellcheck 這樣的工具可以幫助你定位sh/bash指令碼中的錯誤。

koalaman/shellcheck at v0.7.1 (github.com)

shellcheck可以給出warning和語法錯誤提示,還能指出哪些地方你沒正確參照等。

[lighthouse@VM-8-17-centos tools]$ shellcheck mcd.sh

In mcd.sh line 1:
mcd(){
^-- SC2148: Tips depend on target shell and yours is unknown. Add a shebang.

  • Shell函數和指令碼的區別

shell函數和指令碼有如下一些不同點:

  1. 函數只能與shell使用相同的語言,指令碼可以使用任意語言。因此在指令碼中包含 shebang 是很重要的。
  2. 函數僅在定義時被載入,指令碼會在每次被執行時載入。這讓函數的載入比指令碼略快一些,但每次修改函數定義,都要重新載入一次。
  3. 函數會在當前的shell環境中執行,指令碼會在單獨的程序中執行。因此,函數可以對環境變數進行更改,比如改變當前工作目錄,指令碼則不行。指令碼需要使用 export 將環境變數匯出,並將值傳遞給環境變數。
  4. 與其他程式語言一樣,函數可以提高程式碼模組性、程式碼複用性並建立清晰性的結構。shell指令碼中往往也會包含它們自己的函數定義。

Shell工具

檢視命令如何使用

  • man命令

給出一個命令,應該怎樣瞭解如何使用這個命令列並找出它的不同的選項呢?最常用的方法是為對應的命令列新增-h--help 標記。另外一個更詳細的方法則是使用man 命令。man命令是手冊(manual)的縮寫,它提供了命令的使用者手冊。

事實上,目前我們給出的所有命令的說明連結,都是網頁版的Linux命令手冊,即使是安裝的第三方命令。當然前提是開發者編寫了手冊並將其包含在了安裝包中。在互動式的、基於字元處理的終端視窗中,一般也可以通過 :help 命令或鍵入 ? 來獲取幫助。

  • tldr (too long don't read)

有時候手冊內容太過詳實,讓我們難以在其中查詢哪些最常用的標記和語法。TLDR pages是一個很不錯的替代品,它提供了一些案例,可以幫助你快速找到正確的選項。

下載tldr:npm install -g tldr

使用npm命令之前要先下載 yum -y install npm

查詢檔案

你當然可以使用ls,但是如果你想查詢一個已經知道名字的檔案或者目錄,我們可以有更好的做法

  • find

linux-find

find大概是每個UNIX系統都有的工具,例如

[lighthouse@VM-8-17-centos tools]$ find . -name src -type d
./project1/src
./project2/src

這裡意為,在當前資料夾.呼叫find,查詢名為src 並且型別為目錄的東西。鍵入以上命令,它就可以在當前目錄遞迴檢視所有符合規則的檔案或者資料夾(find預設遞迴指定目錄)。

find還有許多有用的flag,比如你甚至可以查詢指定格式的檔案路徑:

[lighthouse@VM-8-17-centos tools]$ find . -path '**/test/*.py' -type f
./project1/src/test/test2.py
./project1/src/test/test1.py
./project1/src/test/test3.py
./project2/src/test/test2.py
./project2/src/test/test1.py
./project2/src/test/test3.py

這裡**是指可以匹配零或者多個目錄名,然後在此路徑下找到拓展名為.py的檔案,並要求它們在一個test資料夾內,同時檢查它是否為F型別(f代表檔案file)

運用不用的flag,可以進行非路徑和非檔名的篩選:

比如可以查詢被修改過的檔案,這裡-mtime代表修改時間,.當前目錄下,最近1天被修改過的東西都會被列出

[lighthouse@VM-8-17-centos tools]$ find . -mtime -1
.
./project1
./project1/src
./project1/src/test
./project1/src/test/test2.py
./project1/src/test/test1.py
./project1/src/test/test3.py
./project2
./project2/src
./project2/src/test
./project2/src/test/test2.py
./project2/src/test/test1.py
./project2/src/test/test3.py
./test
./mcd.sh

你甚至可以使用其他條件,比如大小,所有者,許可權等等。

強大的是,find不僅可以查詢東西,找到之後還可以做別的:例如

我們可以在當前目錄下查詢所有擴充套件名為.tmp的檔案,然後要求find對於所有這些檔案,執行rm命令

[lighthouse@VM-8-17-centos tools]$ find . -name "*.tmp"
./project1/src/test/test3.tmp
./project1/src/test/test1.tmp
./project1/src/test/test2.tmp
./project2/src/test/test3.tmp
./project2/src/test/test1.tmp
./project2/src/test/test2.tmp
[lighthouse@VM-8-17-centos tools]$ find . -name "*.tmp" -exec rm {} \;
[lighthouse@VM-8-17-centos tools]$ echo $?
0
[lighthouse@VM-8-17-centos tools]$ find . -name "*.tmp"
[lighthouse@VM-8-17-centos tools]$ 

如上,執行find . -name "*.tmp" -exec rm {} \;後,對應的tmp檔案都被刪除了。

  • fd

fd 是一個更簡單、更快速、更友好的程式,它可以用來作為find的替代品。它有很多不錯的預設設定,例如輸出著色、預設支援正則匹配、支援unicode並且我認為它的語法更符合直覺。以模式PATTERN 搜尋的語法是 fd PATTERN

[lighthouse@VM-8-17-centos tools]$ fd ".*py"
project1/src/test/test1.py
project1/src/test/test2.py
project1/src/test/test3.py
project2/src/test/test1.py
project2/src/test/test2.py
project2/src/test/test3.py
  • locate

大多數人都認為 findfd 已經很好用了,但是有的人可能想知道,我們是不是可以有更高效的方法,例如不要每次都搜尋檔案而是通過編譯索引或建立資料庫的方式來實現更加快速地搜尋。

這就要靠 locate 了。 locate 使用一個由 updatedb負責更新的資料庫,在大多數系統中 updatedb 都會通過 cron 每日更新。這便需要我們在速度和時效性之間作出權衡。而且,find 和類似的工具可以通過別的屬性比如檔案大小、修改時間或是許可權來查詢檔案,locate則只能通過檔名。 這裡有一個更詳細的對比。

查詢程式碼

查詢檔案是很有用的技能,但是很多時候你的目標其實是檢視檔案的內容。常見的場景是查詢具有匹配某種模式的全部檔案,並找它們的位置。

  • grep

grep是用於對輸入文字進行匹配的通用工具。

[lighthouse@VM-8-17-centos tools]$ grep foobar mcd.sh 
# foobar

使用-R可以遞迴地搜尋

[lighthouse@VM-8-17-centos tools]$ grep -R foobar .
./example.sh:        grep foobar "$file" > /dev/null 2> /dev/null
./example.sh:                echo "File $file does not have any foobar, adding one"
./example.sh:                echo "# foobar" >> "$file"
./hello.txt:# foobar
./mcd.sh:# foobar

grep 有很多選項,這也使它成為一個非常全能的工具。 -C :獲取查詢結果的上下文(Context);-v 將對結果進行反選(Invert),也就是輸出不匹配的結果。舉例來說, grep -C 5 會輸出匹配結果前後五行。當需要搜尋大量檔案的時候,使用 -R 會遞迴地進入子目錄並搜尋所有的文字檔案。但是也有很多辦法可以對 grep -R 進行改進,例如使其忽略.git 資料夾,使用多CPU等等。

  • rg(ripgrep)

此外還出現了很多grep的替代品,包括 ack, agrg。它們都特別好用,但是功能也都差不多,比較常用的是 ripgrep (rg) ,因為它速度快,而且用法非常符合直覺。

rg安裝

[lighthouse@VM-8-17-centos tools]$ rg "foobar" -t sh ~/
/home/lighthouse/missing/tools/mcd.sh
5:# foobar

/home/lighthouse/missing/tools/example.sh
8:        grep foobar "$file" > /dev/null 2> /dev/null
12:                echo "File $file does not have any foobar, adding one"
13:                echo "# foobar" >> "$file"

如上,該命令在~/目錄下搜尋型別(-t即type)為sh,並且檔案內有「foobar」子串的檔案。

rg不僅能找到對應檔案,還能精確到匹配的行,比起使用grep,它還增加了程式碼彩色顯示和檔案處理啥的,也有Unicode支援,並且執行很快。

rg有許多有用的flag,比如說你想要點上下文(匹配內容的附近內容),例如:

[lighthouse@VM-8-17-centos tools]$ rg "foobar" -t sh -C 5 ~/
/home/lighthouse/missing/tools/mcd.sh
1-mcd(){
2-	mkdir -p "$1"
3-	cd "$1"
4-}
5:# foobar

/home/lighthouse/missing/tools/example.sh
3-echo "Start program at $(date)" # Date will be substituted
4-
5-echo "Running program $0 with $# arguments with pid $$"
6-
7-for file in "$@";do
8:        grep foobar "$file" > /dev/null 2> /dev/null
9-        # When pattern is not found,grep has exit status
10-        # We redirect STDOUT and STDERR to a null register ..
11-        if [[ "$?" -ne 0 ]]; then
12:                echo "File $file does not have any foobar, adding one"
13:                echo "# foobar" >> "$file"
14-        fi      
15-done

如上,我們加上-C [num](C意為context),不僅能夠搜尋到匹配內容,還能對每一個匹配的內容顯示其前後[num]行的內容。這樣你就可以知道匹配內容大概在什麼位置,它周圍都是什麼內容。這個功能在查詢在哪呼叫了什麼函數 上十分有用。

我們也可以使用一個更高階的用法:

-u意為不忽略隱藏檔案,--files-without-match是列印出所有不匹配這個pattern的內容,'#!'的意思是匹配有#!的內容。也就是說,我們在搜尋沒有shebang的檔案。

[lighthouse@VM-8-17-centos tools]$ rg -u --files-without-match '#!' -t sh
mcd.sh

此外rg還有些好用的flag,比如--stats這個flag,

[lighthouse@VM-8-17-centos tools]$ rg "foobar" -t sh -C 5 --stats ~/
/home/lighthouse/missing/tools/mcd.sh
1-mcd(){
2-	mkdir -p "$1"
3-	cd "$1"
4-}
5:# foobar

/home/lighthouse/missing/tools/example.sh
3-echo "Start program at $(date)" # Date will be substituted
4-
5-echo "Running program $0 with $# arguments with pid $$"
6-
7-for file in "$@";do
8:        grep foobar "$file" > /dev/null 2> /dev/null
9-        # When pattern is not found,grep has exit status
10-        # We redirect STDOUT and STDERR to a null register ..
11-        if [[ "$?" -ne 0 ]]; then
12:                echo "File $file does not have any foobar, adding one"
13:                echo "# foobar" >> "$file"
14-        fi      
15-done

4 matches
4 matched lines
2 files contained matches
5 files searched
643 bytes printed
978 bytes searched
0.000054 seconds spent searching
0.002657 seconds

如上,它除了搜尋結果之外,還可以輸出一些資訊。比如成功匹配了多少行,查詢了多少行和多少檔案,列印了多少byte等。

  • ack

ack也是grep的一個替代工具,還有ag 。當然這些工具都是可以替換的,只要會使用即可。

查詢shell命令

  • 向上箭頭

首先,按向上的方向鍵會顯示你使用過的上一條命令,繼續按上鍵則會遍歷整個歷史記錄。

向上箭頭並不是很有效率,所以bash有一些更加簡單的方法。

  • history

它會列印出你的命令歷史記錄,當然一般來講這會輸出非常多的記錄,你可以使用管道和grep來篩選。

[lighthouse@VM-8-17-centos tools]$ history | grep echo
   74  2022-12-29 01:16:27 echo $?
  112  2022-12-29 01:45:37 echo "# foobar" >> mdc.sh
  115  2022-12-29 01:46:01 echo "# foobar" >> mcd.sh
  126  2022-12-29 01:50:42 echo "hello,i am a txt file" > hello.txt
  197  2022-12-30 01:06:13 history | grep echo
  • Ctrl+R

基本上,所有Shell都會預設把Ctrl+R這個組合鍵設成(按執行時間)倒敘搜尋(backward search)

我們開啟(按ctrl+r)倒敘搜尋,然後輸入echo,就會找到與之匹配的命令,如果我們接著按ctrl+r,就會倒著往前搜尋匹配的命令,也可以重新執行命令。

  • fzf

Ctrl+R 可以配合 fzf 使用。fzf 是一個通用對模糊查詢工具,它可以和很多命令一起使用。這裡我們可以對歷史命令進行模糊查詢並將結果以賞心悅目的格式輸出

  • 基於歷史的自動補全

另外一個和歷史命令相關的技巧我喜歡稱之為基於歷史的自動補全。 這一特性最初是由 fish shell 建立的,它可以根據你最近使用過的開頭相同的命令,動態地對當前對shell命令進行補全。這一功能在 zsh 中也可以使用,它可以極大的提高使用者體驗。

你可以修改 shell history 的行為,例如,如果在命令的開頭加上一個空格,它就不會被加進shell記錄中。當你輸入包含密碼或是其他敏感資訊的命令時會用到這一特性。 為此你需要在.bashrc中新增HISTCONTROL=ignorespace或者向.zshrc 新增 setopt HIST_IGNORE_SPACE。 如果你不小心忘了在前面加空格,可以通過編輯。bash_history.zhistory 來手動地從歷史記錄中移除那一項。

資料夾導航

你可以使用ls -R遞迴地列出某目錄下所有的檔案和目錄,但是這樣列出的東西比較難理解。

  • tree

有一個叫tree的工具可以以比較友好的格式列印出目錄的結構。

centos安裝:sudo yum -y install tree

[lighthouse@VM-8-17-centos tools]$ tree /home
/home
`-- lighthouse
    `-- missing
        `-- tools
            |-- example.sh
            |-- hello.txt
            |-- mcd.sh
            |-- project1
            |   `-- src
            |       `-- test
            |           |-- test1.py
            |           |-- test2.py
            |           `-- test3.py
            |-- project2
            |   `-- src
            |       `-- test
            |           |-- test1.py
            |           |-- test2.py
            |           `-- test3.py
            `-- test

10 directories, 9 files
  • broot

broot也是做差不多的事情,但是比起列出所有檔案,它會提示[還有更多檔案,未列出]。你可以輸入字元,broot可以模糊匹配符合條件的檔案,並進行動態顯示。這樣你就可以快速的選擇和定位。

  • nnn

nnn 預設列出執行 nnn 的當前目錄的檔案和資料夾。 資料夾列在頂部,而檔案列在底部。而且是一個互動性的視窗,你可以通過向左箭頭返回上一級目錄,通過向右箭頭到達子目錄。按q即可退出視窗。

centos 安裝 nnn :sudo yum install nnn

image-20221230014444510
  • ranger

ranger 是一個基於文字的由 Python 編寫的檔案管理器。不同層級的目錄分別在一個面板的三列中進行展示. 可以通過快捷鍵, 書籤, 滑鼠以及歷史命令在它們之間移動. 當選中檔案或目錄時, 會自動顯示檔案或目錄的內容。


由於本課程的目的是儘可能對你的日常習慣進行優化。因此,我們可以使用fasdautojump 這兩個工具來查詢最常用或最近使用的檔案和目錄。

Fasd 基於 frecency 對檔案和檔案排序,也就是說它會同時針對頻率(frequency)和時效(recency)進行排序。預設情況下,fasd使用命令 z 幫助我們快速切換到最常存取的目錄。例如, 如果您經常存取/home/user/files/cool_project 目錄,那麼可以直接使用 z cool 跳轉到該目錄。對於 autojump,則使用j cool代替即可。

練習

  1. 閱讀 man ls ,然後使用ls 命令進行如下操作:

    • 所有檔案(包括隱藏檔案)
    • 檔案列印以人類可以理解的格式輸出 (例如,使用454M 而不是 454279954)
    • 檔案以最近存取順序排序
    • 以彩色文字顯示輸出結果

    典型輸出如下:

     -rw-r--r--   1 user group 1.1M Jan 14 09:53 baz
     drwxr-xr-x   5 user group  160 Jan 14 09:53 .
     -rw-r--r--   1 user group  514 Jan 14 06:42 bar
     -rw-r--r--   1 user group 106M Jan 13 12:12 foo
     drwx------+ 47 user group 1.5K Jan 12 18:08 ..
    

    練習:

    (1) 顯示包括隱藏檔案

    -a, --all
    do not ignore entries starting with .

    [lighthouse@VM-8-17-centos tools]$ ls -a
    .  ..  example.sh  hello.txt  mcd.sh  project1  project2  test
    

    (2) 檔案以人類可以理解的格式輸出

    -h, --human-readable
    with -l, print sizes in human readable format (e.g., 1K 234M 2G)

    [lighthouse@VM-8-17-centos tools]$ ls -hl
    total 24K
    -rwxrwxr-- 1 lighthouse lighthouse  494 Dec 29 01:49 example.sh
    -rw-rw-r-- 1 lighthouse lighthouse   31 Dec 29 01:55 hello.txt
    -rw-rwxr-- 1 lighthouse lighthouse   42 Dec 29 01:46 mcd.sh
    drwxrwxr-x 3 lighthouse lighthouse 4.0K Dec 29 00:47 project1
    drwxrwxr-x 3 lighthouse lighthouse 4.0K Dec 29 00:47 project2
    drwxrwxr-x 2 lighthouse lighthouse 4.0K Dec 29 00:55 test
    

    (3) 檔案以最近存取順序排序

    -t sort by modification time, newest first

    [lighthouse@VM-8-17-centos tools]$ ls -lt
    total 24
    -rw-rw-r-- 1 lighthouse lighthouse   31 Dec 29 01:55 hello.txt
    -rwxrwxr-- 1 lighthouse lighthouse  494 Dec 29 01:49 example.sh
    -rw-rwxr-- 1 lighthouse lighthouse   42 Dec 29 01:46 mcd.sh
    drwxrwxr-x 2 lighthouse lighthouse 4096 Dec 29 00:55 test
    drwxrwxr-x 3 lighthouse lighthouse 4096 Dec 29 00:47 project1
    drwxrwxr-x 3 lighthouse lighthouse 4096 Dec 29 00:47 project2
    

    (4) 以彩色文字顯示輸出結果

    --color[=WHEN]
    colorize the output; WHEN can be 'never', 'auto', or 'always' (the default); more info below

    [lighthouse@VM-8-17-centos tools]$ ls --color=auto
    example.sh  hello.txt  mcd.sh  project1  project2  test
    

    綜合:

    [lighthouse@VM-8-17-centos tools]$ ls -laht --color=auto
    total 32K
    drwxrwxr-x 5 lighthouse lighthouse 4.0K Dec 30 01:14 .
    -rw-rw-r-- 1 lighthouse lighthouse   31 Dec 29 01:55 hello.txt
    -rwxrwxr-- 1 lighthouse lighthouse  494 Dec 29 01:49 example.sh
    -rw-rwxr-- 1 lighthouse lighthouse   42 Dec 29 01:46 mcd.sh
    drwxrwxr-x 2 lighthouse lighthouse 4.0K Dec 29 00:55 test
    drwxrwxr-x 3 lighthouse lighthouse 4.0K Dec 29 00:47 project1
    drwxrwxr-x 3 lighthouse lighthouse 4.0K Dec 29 00:47 project2
    drwxrwxr-x 3 lighthouse lighthouse 4.0K Dec 29 00:46 ..
    
  2. 編寫兩個bash函數 marcopolo 執行下面的操作。 每當你執行 marco 時,當前的工作目錄應當以某種形式儲存,當執行 polo 時,無論現在處在什麼目錄下,都應當 cd 回到當時執行 marco 的目錄。 為了方便debug,你可以把程式碼寫在單獨的檔案 marco.sh 中,並通過 source marco.sh命令,(重新)載入函數。

    練習:

    marco.sh:

    marco(){
    echo "$(pwd)" > ~/pwd.txt
    }
    
    polo(){
    jump=$(cat ~/pwd.txt)
    # 使用$(命令)的方式可以賦給變數
    cd "$jump"
    echo "You had alread jump to -->$jump"
    }
    

    測試:

    [lighthouse@VM-8-17-centos tools]$ source marco.sh
    [lighthouse@VM-8-17-centos tools]$ marco
    [lighthouse@VM-8-17-centos tools]$ cd /
    [lighthouse@VM-8-17-centos /]$ polo
    You had alread jump to -->/home/lighthouse/missing/tools
    [lighthouse@VM-8-17-centos tools]$ 
    
  3. 假設您有一個命令,它很少出錯。因此為了在出錯時能夠對其進行偵錯,需要花費大量的時間重現錯誤並捕獲輸出。 編寫一段bash指令碼,執行如下的指令碼直到它出錯,將它的標準輸出和標準錯誤流記錄到檔案,並在最後輸出所有內容。 加分項:報告指令碼在失敗前共執行了多少次。

     #!/usr/bin/env bash
    
     n=$(( RANDOM % 100 ))
    
     if [[ n -eq 42 ]]; then
        echo "Something went wrong"
        >&2 echo "The error was using magic numbers"
        exit 1
     fi
    
     echo "Everything went according to plan"
    

    練習:

    上述指令碼的意思是,取一個亂數(RANDOM變數用於生成0~32767之前的任意亂數),亂數模100。如果結果等於42,就輸出兩句話,然後返回1退出碼;否則就輸出」Everything went according to plan「

    這裡的>&2的意思是 將標準輸出1和標準錯誤輸出2 都重定向到終端中(標準輸出或標準錯誤輸出的目的地預設都為終端)

    Linux shell標準輸入,標準輸出,錯誤輸出

    run.sh(buggy.sh為題目的指令碼名)

     count=1
    
     while true
     do
         ./buggy.sh 1>> out.log 2>&1 #把stout和sterr一起重定向到out.log檔案中(追加)
         if [[ $? -ne 0 ]]; then
    	 	 echo "執行錯誤,記錄在out.log中"
             echo "共執行 $count 次"
             break
         fi
         ((count++))
    
     done
    
    [lighthouse@VM-8-17-centos tools]$ ./run.sh 
    執行錯誤,記錄在out.log中
    共執行 82 次
    [lighthouse@VM-8-17-centos tools]$ ./run.sh 
    執行錯誤,記錄在out.log中
    共執行 42 次
    
  4. 本節課我們講解的 find 命令中的 -exec 引數非常強大,它可以對我們查詢的檔案進行操作。但是,如果我們要對所有檔案進行操作呢?例如建立一個zip壓縮檔案?我們已經知道,命令列可以從引數或標準輸入接受輸入。在用管道連線命令時,我們將標準輸出和標準輸入連線起來,但是有些命令,例如tar 則需要從引數接受輸入。這裡我們可以使用xargs 命令,它可以使用標準輸入中的內容作為引數。 例如 ls | xargs rm 會刪除當前目錄中的所有檔案。

    您的任務是編寫一個命令,它可以遞迴地查詢資料夾中所有的HTML檔案,並將它們壓縮成zip檔案。注意,即使檔名中包含空格,您的命令也應該能夠正確執行(提示:檢視 xargs的引數-d,譯註:MacOS 上的 xargs沒有-d檢視這個issue

    如果您使用的是 MacOS,請注意預設的 BSD findGNU coreutils 中的是不一樣的。你可以為find新增-print0選項,併為xargs新增-0選項。作為 Mac 使用者,您需要注意 mac 系統自帶的命令列工具和 GNU 中對應的工具是有區別的;如果你想使用 GNU 版本的工具,也可以使用 brew 來安裝

    練習:

    事先在當前資料夾下建立了一些html檔案(包括帶有空格的he llo.html)

    [lighthouse@VM-8-17-centos question4]$ tree 
    .
    |-- he\ llo.html
    |-- index.html
    |-- project1
    |   |-- test
    |   |-- test1.html
    |   |-- test2.html
    |   `-- test3.html
    |-- project2
    |   |-- test
    |   |-- test1.html
    |   |-- test2.html
    |   `-- test3.html
    |-- test1.html
    |-- test2.html
    |-- test3.html
    |-- test4.html
    |-- test5.html
    |-- test6.html
    |-- test7.html
    |-- test8.html
    `-- test9.html
    
    4 directories, 17 files
    

    使用命令:

    [lighthouse@VM-8-17-centos question4]$ find .  -name "*.html" | xargs -d '\n' tar -cf html.zip
    

    檢視壓縮包內容:

    可以看到包括有空格檔名的html在內全部壓縮成功

    [lighthouse@VM-8-17-centos question4]$ tar -tf html.zip 
    ./project1/test3.html
    ./project1/test2.html
    ./project1/test1.html
    ./test6.html
    ./test3.html
    ./test8.html
    ./test4.html
    ./test9.html
    ./project2/test3.html
    ./project2/test2.html
    ./project2/test1.html
    ./test5.html
    ./he llo.html
    ./test2.html
    ./index.html
    ./test1.html
    ./test7.html
    

    xargs使用教學 Linux下檢視壓縮檔案內容的 10 種方法

    使用 tar -tf 命令可以在不提取 tar 檔案的情況下檢視壓縮包內容。

  5. (進階)編寫一個命令或指令碼遞迴的查詢資料夾中最近使用的檔案。更通用的做法,你可以按照最近的使用時間列出檔案嗎?

    [lighthouse@VM-8-17-centos question4]$ find . -type f -mmin -120 | xargs -d '\n'  ls -tl | head -3
    -rw-rw-r-- 1 lighthouse lighthouse 10240 Jan  3 22:01 ./html.zip
    -rw-rw-r-- 1 lighthouse lighthouse     0 Jan  3 21:36 ./he llo.html
    -rw-rw-r-- 1 lighthouse lighthouse     0 Jan  3 21:15 ./project1/test1.html