在程式語言基礎上的 linux shell 指令教學
shell 的設計理念以 unix 環境中的程序為中心, 發展出參數、管道等觀念。 並且最重要的,是一門可以應用在電腦日常使用上的實際的程式語言。 對簡單學習過 c 語言的人, 配合 c 從 shell 認識電腦是一條十分有意義的道路。 本簡報從使用電腦的角度,帶領有 c 語言基礎的人認識 shell。
facebook 個人心得
這算我玩 linux 以來的小小願望吧。
一直覺得學校的資訊教育打不到點。雖然高中學過 C 語言,但那時的演算法競賽完全無法挑起我的興趣。直到大學接觸到 linux 和 shell 後才感受到什麼叫 學會使用電腦 。所以有人問怎麼學程式設計,我都直接回灌 linux,過程會用到指令,指令就是電腦運作最真實的模樣。最終,指令也會引導你走向程式語言,任何一種程式語言都可以;你才算是學會電腦。
現在單純學程式語言的很多,但會有個門檻是要能跨出 IDE,要知道可以怎麼應用。至少這是現在我回想高中時的心得。所以我比較推 shell。
希望這堂課能引起共鳴,至少讓其它人聽懂我在說什麼。
facebook 社課說明
在程式語言基礎上的 linux shell 指令教學
- 時間:2019-10-04 19:00-21:20 (五)
- 地點:交通大學資訊服務中心 電腦教室三
- 預期聽眾:
- 學過基本程式語言,卻覺得還是什麼都做不了的人。
- 對指令介面嚮往的新手到老手。
- 不懂程式語言,想要實用地學電腦的人。
- 要學 git 的人;因為 git 要靠指令操作。
是否有經驗看著 linux 大神兩眼映照在漆黑的終端機中, 雙手運指如飛,指令在螢幕上傾洩而下的的模樣? 又或是否懷疑過,為什麼有一群人放著好好的 windows 不用, 而要特別在電腦上安裝另一種作業系統 linux? linux 的 shell,就是其中一個常見的答案。
linux 強大的 shell 指令介面是許多人對 linux 愛不釋手的原因; 但對新手而言,過度依靠 shell 也是讓 linux 難以入門的因素。 shell 的學習門檻高,使用上需要熟悉大量的指令,較圖形介面難以上手。
本次社課將從日常使用講起, 介紹 shell 如何是一門貼近使用者與務實的語言, 並穿插 shell 運作的基本邏輯, 使聽眾能將課堂所學的程式設計,實際運用到電腦的日常使用中, 自力成為 linux 大師。
靠北板發文
為什麼同學上完一學期 python 還是電腦白痴啊? 不過就簡單的檔案處理,寫個 one-liner 去跑很難嗎? 抱怨函數沒看過,也不會翻文件, 沒寫過如我不翻文件都看得懂大半了。 深深覺得台灣資訊教育真的缺了某個很重要的部份。
真想推薦他們 CCCA 這周五 10/4 19:00 在資訊中心電腦教室四的社課: 《在程式語言基礎上的 linux shell 指令教學》 聽說會從日常使用講起,介紹 shell 如何是一門貼近使用者與務實的語言, 讓聽眾能將課堂所學的程式設計,實際運用到電腦的日常使用中。 https://www.facebook.com/NCTUCCCA/posts/768329356934169
但主要還是要先有勇氣跨進 linux,至少裝個 cygwin 或 wsl。
大綱
- 可以實際應用的程式設計
- shell 入門
- 程式與命令
- shell 編程
- 改檔名的啟示
- 圖形介面
- 結論
可以實際應用的程式設計
高中電腦多半從 c 這種基本的,或其它 簡單的 程式語言教起, 但這些語言有個很大的問題, 他們都無法讓學生應用在日常的電腦操作上。
c 可以寫什麼
- scanf printf
- fopen
問題的關鍵
- 日常使用電腦的操作,無法輕易用 c 達成。
- 學校的教學偏重在理論,而不是實務應用。
日常的操作
- 複製檔案
- 打開網頁
- 壓縮檔案
什麼是 shell
- 日常使用的稱為 圖形介面 。
- 在圖形介面發明以前,人機之間以文字指令溝通。
- 作業系統內部是一支執行中的程式, 而 shell 用來稱呼人與作業系統溝通的介面。
指令的特性
- 學習門檻較高。
- 由於指令只是一連串文字,可以把要執行的指令存成檔案,方便執行。
- 本質是一種特化的程式語言。
指令與圖形介面等價
- 在沒有圖形介面以前,人們還是使用電腦。
- 絕大多數在圖形介面能做到的事,指令也都能做到。
- 雖然無法做到 開啟視窗然後按下某個按鈕 之類的圖形介面操作。
shell 入門
指令是不含空白的一連串字元,一般包含底線與連字號。
pwd
ls
帶有參數的指令
參數以空白分隔
mv config.txt config_backup.txt
cp config_origin.txt config.txt
rm log.txt
移動到不同資料夾
cd my-dir
cd ..
cd /home
cd ../usr/bin
何謂開啟檔案?
- 用 fopen 打開讀取或寫入
- 更改檔案內容
- 開啟檔案對應的圖形程式
- 用滑鼠點二下
開啟圖形程式
gedit
lowriter
firefox
用圖形程式開啟檔案
gedit file.txt
lowriter document.odt
firefox print.pdf
統一的指令
# linux
xdg-open file.odt
REM windows
start file.doc
REM or directly type filename
file.doc
快捷鍵
- 指令到很複雜的話,敲鍵盤很累的。
- 所以 shell 多半有快捷鍵可以用, bash 中處理這些快捷鍵的程式稱為 readline。
自動補全
- tab 鍵可以自動補全打到一半的單字
- 基本的命令可以自動補全,例如打 pyt 之後按 tab 就會補全成 python。
- 檔名也可以自動補全,例如
xdg-open file.o
之後按 tab 會依存在的檔案補全。
上一個指令
- 方向鍵上下可以在歷史記錄中瀏覽
- 或是用 ctrl-p ctrl-n
在歷史中搜尋
- ctrl-r 可以在歷史中搜尋
- 如果找到的不是你要的,可以再按一次 ctrl-r 再往前找
- 或是把搜尋字串打完整一點
用指令直接更改檔案
上面都是用指令 打開 一個圖形介面。 那能不能不透過圖形介面, 直接用指令完成對檔案的操作呢?
處理壓縮檔
# create zip homework.zip,
# which contain code.c report.odt Makefile
zip homework.zip code.c report.odt Makefile
看壓縮檔的內容
unzip homework.zip # directly unzip
unzip -l homework.zip # list content of zip
更改內容
# delete file in zip
zip -d homework.zip Makefile
# update file in zip
zip -u homework.zip code.c
選項
- 在 unix 下,多數程式會把以
-
開頭的參數當作 選項 。 - 選項用來改變程式的行為。
短選項與連寫
- 最早選項都只有一個字
-h
-a
-t
。 - 工程師都很懶,所以慣例上短選項可以寫在一起
-hat
。
bsd 的長選項
- 後來程式越來越複雜,26 個選項不夠用。
- bsd 發明長選項,選項從字元改成單字
-help
-all
。 - 因此連寫不能用了。
gnu 的長選項
- gnu 覺得這樣不向下相容,不優。
- 規定為了與短選項區隔,長選項應該要用二個
-
開頭,--help
--all
。 - 程式可以同時有短選項與長選項,短選項仍可以連寫。
但選項的格式只是慣例
- 程式看到的只是一連串的字串格式參數,只是某些參數用
-
開頭。 - 程式作者可以把程式設計成用
/
當選項開頭,用\
當路徑分隔符。 (windows: 誰叫我?) dd
就是一支不同風格的常用程式。
選項查詢
要怎麼知道命令怎麼下?有哪些選項?
google- 求助選項:
zip --help
或zip -h
- 線上手冊:
man zip
- 搜尋手冊:
apropos zip
求助選項
多數程式會有一個求助選項,
看到 --help
或 -h
就會直接輸出簡短的說明。
當然也不是所有程式都有,還是那句話,看作者。
手冊
- unix 系統中自帶有說明書,說明書可以用一支叫
man
的程式閱讀。 - man 會開啟一個介面,可以用方向鍵翻閱,用
q
可以離開。
搜尋合適的指令
- apropos 可以用關鍵字搜尋手冊。
- 一般有手冊也就代表有指令可以用。
第一支腳本
# pack homework files into zip
zip homework.zip report.odt code.c
執行腳本
sh script.sh
加入提示
echo start packing file
zip homework.zip report.odt code.c
echo finish packing file
echo
- 寫在腳本裡的指令是直接執行,而執行時不會顯示現在在執行什麼。
(dos 會,可以用
echo on
echo off
控制。) - 所以要用 echo 把想顯示的文字 回音 出來。
輸出字串
- echo 就是單純輸出字串,對特殊字元和格式化的支援有限。
- printf 可以做到接近 c printf 的五花八門功能。
printf
printf "file-%03d.txt\n" 12 # file-012.txt
輸出太長怎麼辦
剛剛說的 apropos
~:$ apropos zip
CPAN::Tarzip (3pm) - internal handling of tar archives for CPAN.pm
Archive::Zip (3pm) - Provide an interface to ZIP archive files.
Archive::Zip::FAQ (3pm) - Answers to a few frequently asked questions about A...
Archive::Zip::MemberRead (3pm) - A wrapper that lets you read Zip archive mem...
Archive::Zip::Tree (3pm) - (DEPRECATED) methods for adding/extracting trees u...
bunzip2 (1) - a block-sorting file compressor, v1.0.6
bzcmp (1) - compare bzip2 compressed files
bzdiff (1) - compare bzip2 compressed files
bzegrep (1) - search possibly bzip2 compressed files for a regular e...
bzfgrep (1) - search possibly bzip2 compressed files for a regular e...
bzgrep (1) - search possibly bzip2 compressed files for a regular e...
bzip2 (1) - a block-sorting file compressor, v1.0.6
bzip2recover (1) - recovers data from damaged bzip2 files
bzless (1) - file perusal filter for crt viewing of bzip2 compresse...
bzmore (1) - file perusal filter for crt viewing of bzip2 compresse...
Compress::Raw::Bzip2 (3perl) - Low-Level Interface to bzip2 compression library
CPAN::Tarzip (3perl) - internal handling of tar archives for CPAN.pm
docker-context-import (1) - Import a context from a tar or zip file
fcrackzip (1) - a Free/Fast Zip Password Cracker
fcrackzipinfo (1) - display zip information
funzip (1) - filter for extracting from a ZIP archive in a pipe
gpg-zip (1) - encrypt or sign files into an archive
gunzip (1) - compress or expand files
gzip (1) - compress or expand files
IO::Compress::Bzip2 (3perl) - Write bzip2 files/buffers
IO::Compress::Gzip (3perl) - Write RFC 1952 files/buffers
IO::Compress::Zip (3perl) - Write zip files/buffers
IO::Uncompress::AnyInflate (3perl) - Uncompress zlib-based (zip, gzip) file/b...
IO::Uncompress::AnyUncompress (3perl) - Uncompress gzip, zip, bzip2 or lzop f...
IO::Uncompress::Bunzip2 (3perl) - Read bzip2 files/buffers
IO::Uncompress::Gunzip (3perl) - Read RFC 1952 files/buffers
IO::Uncompress::Unzip (3perl) - Read zip files/buffers
MIME::Decoder::Gzip64 (3pm) - decode a "base64" gzip stream
p7zip (1) - Wrapper on 7-Zip file archiver with high compression r...
pbzip2 (1) - parallel bzip2 file compressor, v1.1.9
PerlIO::gzip (3pm) - Perl extension to provide a PerlIO layer to gzip/gunzip
preunzip (1) - prefix delta compressor for Aspell
prezip (1) - prefix delta compressor for Aspell
prezip-bin (1) - prefix zip delta word list compressor/decompressor
unzip (1) - list, test and extract compressed files in a ZIP archive
unzipsfx (1) - self-extracting stub for prepending to ZIP archives
zforce (1) - force a '.gz' extension on all gzip files
zip (1) - package and compress (archive) files
zipcloak (1) - encrypt entries in a zipfile
zipdetails (1) - display the internal structure of zip files
zipgrep (1) - search files in a ZIP archive for lines matching a pat...
zipinfo (1) - list detailed information about a ZIP archive
zipnote (1) - write the comments in zipfile to stdout, edit comments...
zipsplit (1) - split a zipfile into smaller zipfiles
用 less 接起來
- 那條直線叫管道符號,可以把前面程式的輸出,丟給後面程式。
- less 的功能就是把過多的輸出接起來,可以上下翻頁慢慢看。
apropos zip | less
其它用來接輸出的程式
apropos zip | head
apropos zip | tail
或是輸入到檔案
apropos zip > apropos-zip.txt
head < apropos-zip.txt
重導向或是直接指定檔名?
head < apropos-zip.txt
head apropos-zip.txt
有關 man
- man 是一支很聰明的程式,會判斷輸出的裝置決定行為。
- 直接呼叫時,man 因為輸出對向是終端機,所以呼叫 less 把內容接起來。
- 如果對向是檔案,man 就會直接輸出文字檔。
man zip # 開啟 less 讓使用者翻閱手冊
man zip > zip.txt # 把手冊存成文字檔
更改檔案內容
上面提過 echo printf,輸出也能重導向到檔案。
echo hey > hey
printf "hello world!" > hey
覆寫與附加
- 一個
>
會寫入,如果檔案已經存在會被清空後寫入新內容。 - 二個
>>
會附加,如果檔案已經存在會附加在檔案結尾。
echo bye >> hey
printf "byby!" >> hey
組合 shell 命令
先前也提過,shell 最大的好處是可以寫成腳本執行, 可以省去用滑鼠點來點去的時間。
命令代換
被 $()
包起來的命令,執行後的結果會被置換到命令中。
用 wc 計算 ls 命令列出的第一個檔案大小。
wc -c $(ls | head -n 1)
變數賦值
sh 中,所有變數內容都是字串。
a=file.txt
b=$(ls | head -n 1)
變數取值
- 要展開變數需要用
$
標記,不然只會被當成單純字串而不是變數名。 - 但賦值時不能有
$
號!
wc -c a # 計算檔名為 a 的大小
wc -c $a # 計算檔名為變數 a 的內容的大小
管道的力量
串連多個小程式,就能快速達到各種功能。
sed 's/[^[:alnum:]]/\n/g' | sort | uniq -c
網頁爬取
url='http://jamesiscurly.pixnet.net/blog/post/295281136-%e6%98%af%e6%97%a5%e6%a2%97%e5%9c%960621'
curl $url | grep -o '<img.*?>'
curl $url | tidy -asxml -q >meme.xml
xpath -e '//img' meme.xml
# or curl $url | tidy -asxml -q | xpath -e '//img'
程式
- stdin 標準輸入、stdout 標準輸出
- argv 參數
簡單的 c
// substr.c
int main(int argc, char **argv) {
// argv
int start;
sscanf(argv[1], "%d", &start); // atoi()
int end;
sscanf(argv[2], "%d", &end);
char string[80];
fgets(string, sizeof(string), stdin); // scanf("%s", string)
for (int i=start; i<end; i++) putchar(string[i]); // stdout
putchar('\n');
return 0;
}
編譯執行
~/code:$ gcc -o substrc substr.c
~/code:$ echo hello world | ./substrc 3 8
lo wo
如何讓程式變成命令
- shell 內有變數
PATH
,輸入的命令會在 PATH 中的目錄中搜尋執行檔。 - 所以,把執行檔丟到 PATH 的任一目錄,
- 或者,把執行檔所在目錄加入 PATH。
萬變不離宗
- 絕大多數的命令都是程式
- 從參數與標準輸入讀取,執行對應的行為。
組合的力量
shell 本質即是組合大量行為單純的程式,從而達到複雜的功能。
shell 編程
shell 是一門完整的程式語言,當然帶有流程控制、迴圈等功能。
流程控制
if pwd | grep "^/home/$USER"
then echo inside home directory
else echo outside home directory
fi
控制原理
- shell 中 if 後接的命令會被執行,並依命令的返回值判斷分支。
- 若回傳 0 即為 true,以外為 false。
c 語言的返回值
int main(int argc, char **argv) {
/* some code */
if (true) return 0;
else return error_code;
}
判斷迴圈
while pgrep wget
do
echo wget is downloading
sleep 1m
done
echo wget finish
while true
do
if pgrep wget
then
echo wget is downloading
sleep 1m
else
echo wget finish
break
fi
done
遍歷迴圈
for number in $(seq 10)
do
file=$(printf "file-%02d.txt" $number)
cat $file >> all.txt
done
判斷的命令
string=abc
test $string = abc # return 0
[ $string = abc ] # as same as test
if [ $n = 0 ]
c 實作
int main(int argc, char **argv) {
return !strcmp(argv[1], argv[3]);
}
gcc -o test test.c
cp test [
執行檔
- 上面的執行檔都是用 c 語言示範
- 因為大家應該都會 c 語言
- 但實務上沒有人會想用 c 語言
hashbang 變身執行檔
- 多數直譯語言執行:
python script.py
sh script.sh
- 如果在文字檔第一行加上
#!
與直譯器的絕對路徑, 就能讓文字檔能被執行:./script.py
./script.sh
- 並且要賦與該檔案可執行權限:
chmod +x script.sh
#!/usr/bin/python
print("hello world!")
#!/bin/sh
echo hello world!
以 test 為例
#!/usr/bin/node
# test.js
if (process.argv[2] == process.argv[4]) process.exit(0)
else process.exit(1)
chmod +x test.js
cp test.js [
選項的解析
另外,一開始有提過長短選項; 不管是哪一種風格選項,解析選項都不是一件容易的事。 有興趣可以自己試試看。 我之前寫的用 sh 解析選項的範例程式。
shell 與通用語言
shell 本身的功能很少,多數都是靠其它程式實現的。 例如上述示範的,字串比較這種基礎功能也可以靠外部實現。
shell 的特性
- 不適合寫複雜的邏輯、數字、字串。
- 缺乏資料結構。
- 但呼叫其它程式很方便。
- 只要可以成為執行檔,都是 shell 指令。
- 有人吐嘈 shell 做點屁大的事都要 fork 一堆進程。
- 如果用管道或命令代換,更會開到子 shell。
在一般程式語言中呼叫其它命令
- 開一個 shell 來執行傳入的字串。
- 跳過 shell,直接執行該程式,參數要自己分割好傳入。
system("zip -u homework.zip code.c");
execlp("zip", "-u", "homework.zip", "code.c");
一般程式語言特性
- 內建功能完整。
- 呼叫其它程式不方便。
shell 是 linux 的統一介面
- 不同程式語言內部有自己互相呼叫的介面。
- 可執行檔是 linux 的統一介面,呼叫要傳入參數、環境變數,處理輸入輸出。
- shell 是執行檔呼叫的最簡單介面。
建議
- 精通 shell,和一門 通用的程式語言 。
- shell 也有很多種,像 bash、fish、power shell。
- shell 受限於系統執行檔所提供的功能。
- 一些比較龐大的軟體會有自己內部語言。
- microsoft office visual basic for applications
- 瀏覽器的 javascript
- 但有時仍可以用通用程式語言溝通,如 libre office libuno
以改檔名為例
- 看似簡單的操作
- 但現成的批次重命名工具功能有限
- 直接用程式語言實作太麻煩
- 用 shell 是最有效的方法
gnss 觀測數據命名格式
- NCTU0100.19o:站名、一年中的日數、0、西元年、o
- NCTU201901100.rnx:站名、年月日、0、rnx
批次工具
- 有完整 regexp 就很了不起了
- 但很多時候還需要改大小寫、條件判斷、數字運算,不是單純字串代換能處理的。
- excel
寫一個 python
- 要 import 套件
- 如何讀取檔名?
os.listdir()
- 如何改檔名?
- 要處理文字、數字轉換
- 每轉換一個資料夾,就寫一次腳本?還不能重用?
直接用 python 輸出 shell 指令
- 不用想怎麼在 python 中執行指令。
- 直接 print 出指令,用貼上的或 pipe 給 shell。
- 尤其是一些複雜的指令,不用想怎麼在 python 中建立壓縮檔, 直接呼叫 zip 指令就好。
print('mv', oldname, newname)
剪下貼上 shell 輸出
有時候,直接把 ls 的輸出複製到檔案, 再用 regexp 改成一堆 mv 還比較快。
ls | vi -
# in vi
# :%s/\(.*\).html/mv \1.html \1.htm/
# :%!sh
直接用 shell 解決
# rename Trim201904010000.rnx to NCTU0910.19o
for file in *
do
old_format=$(date -d $(echo $file | substr 4 12) +"NCTU%03j0.%02yo")
echo mv $file $old_format
mv $file $old_format
done
shell 的優勢
- 自動補全、萬用字元、REPL 可以即時看到結果。
- 加個 echo 先確認會跑出什麼東西。
- 極適合寫這種一次性用完即丟的程式。
- bash 的歷史記錄可以讓你翻出不久前打過的命令,稍作修改就能重用。
其它程式語言
- 每次寫這種東西都要先匯入類似的函式庫、寫一堆類似的 code。
- 但又很難抽象化成可以重用的程式。
- 留下一大堆不知道該不該刪的程式。
- 擅長 one-liner 的語言:perl awk。
seq 10 | perl -ne 'print if $_ % 2'
glob 萬用字元
- shell 會依據檔名展開某些特殊字元
*
: 任意個任意字元?
: 一個任意字元[a-ez0-9]
: 一個範圍內的字元
萬用字元用例
mv * ../backup/
rename 's/.tar.gz$/.tgz/' *.tar.gz
# match filename longer then n word
for zip in ??????????????????????????*.zip
do
basename=$(basename $(unzip -l $zip | grep -o '........\.csv') .pdf)
mv $zip $basename.zip
done
GUI 程式
- 現代很多人只把 gui 程式認為是 真正的 程式。
- 那我們就用 shell 來寫 gui 吧!
彈出視窗的命令
- 瀏覽器有
alert()
confirm()
prompt()
可以 彈出對話框 , - shell 有沒有類似的東西呢?
xmessage
if xmessage -buttons yes:0,no:2 'do you want to clean trash can?'
then gio trash --empty
fi
zenity
zenity --info --text 'hello zenity!'
if zenity --question --text 'do you want to give me your name?'
then
name=$(zenity --entry --text 'what is your name?')
zenity --info --text "hello $name"
fi
其它 x window 好用工具
- xdotool: 用來摸擬滑鼠點擊、鍵盤事件的命令。
- wmctrl: 用來控制視窗放大縮小。
- xprop: 比較底層,用來查詢各視窗的屬性、控制的程序。
結論
- shell 是一門 實際 且 能與所學結合 的語言。
- 社課很短,能講的東西有限。
- 希望我的表達能力正常無礙。
指令示範
把系統大檔案移到另一個硬碟
find -size +1G | while read path
do
mv $path /mnt/backup/
ln -s /mnt/backup/$(basename $path) $path
done
每 30 分鐘提醒一次眼睛休息
(
while sleep 30m
do zenity --info --text "30 分鐘了,眼睛休息一下吧!"
done
) &
延伸閱讀
- 命令列讀本 , 用輕鬆、務實的角度入門 shell 使用。
- 高級 Bash 脚本编程指南 , 完全理解 bash 的功能,了解不同語法間的細微差異。
- 考驗對 shell 常指令的理解 。