主題 2 Shell工具和腳本 Shell 工具和腳本 · the missing semester of your cs education (missing-semester-cn.github.io) Shell腳本 shell 腳本是一種更加複雜度的工具。 定義變數 在bash中為變數賦值的 ...
主題 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
, while
和 for
這些控制流關鍵字。同樣地, 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.py
,test2.py
,test3.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腳本中的錯誤。
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函數和腳本有如下一些不同點:
- 函數只能與shell使用相同的語言,腳本可以使用任意語言。因此在腳本中包含
shebang
是很重要的。 - 函數僅在定義時被載入,腳本會在每次被執行時載入。這讓函數的載入比腳本略快一些,但每次修改函數定義,都要重新載入一次。
- 函數會在當前的shell環境中執行,腳本會在單獨的進程中執行。因此,函數可以對環境變數進行更改,比如改變當前工作目錄,腳本則不行。腳本需要使用
export
將環境變數導出,並將值傳遞給環境變數。 - 與其他程式語言一樣,函數可以提高代碼模塊性、代碼復用性並創建清晰性的結構。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
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
大多數人都認為 find
和 fd
已經很好用了,但是有的人可能想知道,我們是不是可以有更高效的方法,例如不要每次都搜索文件而是通過編譯索引或建立資料庫的方式來實現更加快速地搜索。
這就要靠 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, ag 和 rg。它們都特別好用,但是功能也都差不多,比較常用的是 ripgrep (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
- ranger
ranger 是一個基於文本的由 Python 編寫的文件管理器。不同層級的目錄分別在一個面板的三列中進行展示. 可以通過快捷鍵, 書簽, 滑鼠以及歷史命令在它們之間移動. 當選中文件或目錄時, 會自動顯示文件或目錄的內容。
由於本課程的目的是儘可能對你的日常習慣進行優化。因此,我們可以使用fasd和 autojump 這兩個工具來查找最常用或最近使用的文件和目錄。
Fasd 基於 frecency 對文件和文件排序,也就是說它會同時針對頻率(frequency)和時效(recency)進行排序。預設情況下,fasd
使用命令 z
幫助我們快速切換到最常訪問的目錄。例如, 如果您經常訪問/home/user/files/cool_project
目錄,那麼可以直接使用 z cool
跳轉到該目錄。對於 autojump,則使用j cool
代替即可。
練習
-
閱讀
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 ..
-
編寫兩個bash函數
marco
和polo
執行下麵的操作。 每當你執行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]$
-
假設您有一個命令,它很少出錯。因此為了在出錯時能夠對其進行調試,需要花費大量的時間重現錯誤並捕獲輸出。 編寫一段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 都重定向到終端中(標準輸出或標準錯誤輸出的目的地預設都為終端)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 次
-
本節課我們講解的
find
命令中的-exec
參數非常強大,它可以對我們查找的文件進行操作。但是,如果我們要對所有文件進行操作呢?例如創建一個zip壓縮文件?我們已經知道,命令行可以從參數或標準輸入接受輸入。在用管道連接命令時,我們將標準輸出和標準輸入連接起來,但是有些命令,例如tar
則需要從參數接受輸入。這裡我們可以使用xargs
命令,它可以使用標準輸入中的內容作為參數。 例如ls | xargs rm
會刪除當前目錄中的所有文件。您的任務是編寫一個命令,它可以遞歸地查找文件夾中所有的HTML文件,並將它們壓縮成zip文件。註意,即使文件名中包含空格,您的命令也應該能夠正確執行(提示:查看
xargs
的參數-d
,譯註:MacOS 上的xargs
沒有-d
,查看這個issue)如果您使用的是 MacOS,請註意預設的 BSD
find
與 GNU 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
文件的情況下查看壓縮包內容。 -
(進階)編寫一個命令或腳本遞歸的查找文件夾中最近使用的文件。更通用的做法,你可以按照最近的使用時間列出文件嗎?
[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