OI 中的小技巧(工具)
- OI 中的小技巧(工具)
- ref
- bash 基礎語法
- 可執行檔案
- 變數
- 管道
- if-else
- for
- while
- bash 應用
- time
- ulimit
- 批次過樣例
- 花絮:freopen 開關
- 對拍器
- 備份工具
- makefile
- 基礎語法
- 傳統題寫法
- 互動題寫法
- 打掃檔案
- 結合 vim
ref
https://zhuanlan.zhihu.com/p/494668063
bash 基礎語法
可執行檔案
在 NOI Linux 或其它 Linux 環境下開啟終端,可以在終端中輸入命令:
you@localhost:~/test$ g++ a.cpp -o main
you@localhost:~/test$ ./main
1 2
3
這裡我們編譯了 a.cpp
檔案為 main
,執行,發現它正確實現了計算 \(a+b\) 的功能。g++
和 ./main
是可執行檔案,在一條命令的開端,後面緊跟的都是引數。g++
在環境變數中,main
在當前目錄下,在 bash 中執行不在環境變數中的可執行檔案需要寫出其路徑,最簡單的方法就是寫為 ./main
。.
表示當前目錄(..
表示上一級目錄),/
是資料夾名與檔名之間的分隔符。以上內容建議百度“Linux 命令列入門”進一步瞭解。
變數
bash 中可以定義變數。
you@localhost:~/test$ a=1
you@localhost:~/test$ echo ${a}
1
a=1
中 a
是變數名,1
這裡當作一個字串。讀取變數 a
的內容,寫作 ${a}
。同樣這些東西建議百度瞭解。
管道
bash 中存在名為管道的東西,作用是將上一個命令的標準輸出傳遞給下一個命令的標準輸入,符號是 |
。
you@localhost:~/test$ echo 1 2 | ./main
3
例如可以 echo 1 2
輸出 1 2
,將其傳遞給 ./main
作為它的標準輸入。更有用的地方是,部分命令如 diff
grep
可以透過指定檔名為 -
(或其它,可以傳遞引數 --help
檢視)以讀取標準輸入作為檔案內容。因此可以幹出這樣的事情:
you@localhost:~/test$ echo 1 2 > in
you@localhost:~/test$ echo 3 > ans
you@localhost:~/test$ ./main < in | diff - ans
you@localhost:~/test$ echo 4 > out
you@localhost:~/test$ ./main < in | diff - out
1c1
< 3
---
> 4
Linux 檔名可以無字尾名。這裡依次建立了三個檔案。第一次執行 ./main
,輸入 1 2
輸出 3
,與檔案 ans
比較相同;第二次執行 ./main
,輸入 1 2
輸出 3
,與檔案 out
比較不同,輸出了錯誤資訊。
if-else
大概這樣寫:if {{condition_command}}; then {{echo "Condition is true"}}; fi
。
注意:{{condition_command}}
返回 0 的時候進入後面的語句塊,返回 0 一般表示這個命令成功執行。這一點與其它語言不同。
可以百度或者 help if
獲得更多幫助。
for
for {{variable}} in {{item1 item2 ...}}; do {{echo "Loop is executed"}}; done
for {{variable}} in {{{from}}..{{to}}..{{step}}}; do {{echo "Loop is executed"}}; done
for {{variable}} in */; do (cd "${{variable}}" || continue; {{echo "Loop is executed"}}) done
可以 sudo apt install tldr
然後 tldr for
獲得更多幫助。
while
while {{condition_command}}; do {{command}}; done
寫在同一行的時候需要分號。do 前面換行可以去掉 do 前分號。done 前換行可以去掉 done 前分號。for 同理。
bash 應用
time
作為內建命令時,time
後面直接加一條命令可以測量其執行時間。
you@localhost:~/test$ time ./main
1 2
3
real 0m0.383s
user 0m0.002s
sys 0m0.000s
作為一個可執行檔案 /usr/bin/time
(等價於 /bin/time
)時,也是同樣用法,但是輸出不同:
you@localhost:~/test$ /bin/time ./main
1 2
3
0.00user 0.00system 0:02.34elapsed 0%CPU (0avgtext+0avgdata 3576maxresident)k
0inputs+0outputs (0major+135minor)pagefaults 0swaps
只需要知道評測時看的是 user time。測量空間時,使用 /bin/time -v
。
you@localhost:~/test$ /bin/time -v ./main
1 2
3
Command being timed: "./main"
User time (seconds): 0.00
System time (seconds): 0.00
Percent of CPU this job got: 0%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:01.07
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 3424
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 134
Voluntary context switches: 2
Involuntary context switches: 0
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
Maximum resident set size (kbytes): 3424
是所測量的。
ulimit
修改終端的資源限制,其中最有用的是修改棧空間:ulimit -s unlimited
表示無線棧空間。unlimited
可以改為任意一個 KB 為單位的數字,如 ulimit -s 1048576
使得棧空間為 1G。
批次過樣例
想象你正在參加一場 OI 比賽,本題的英文名為 station
,所以你編寫了 station.cpp
作為提交的程式碼,現在想要測試之。已經去掉了 freopen。
可以編寫 test.sh
:
#!/bin/bash
ln -s ../../down/station/* .
g++ station.cpp -o station -std=c++14 -O2
for i in {1..2}; do
time ./station <station$i.in | diff - station$i.out -Bsbq
done
第一行稱為 shebang,指定使用 bash 解析這個檔案。
第二行將所有樣例檔案軟連結到當前目錄,可以搜尋一下什麼是軟連結。這裡將存放在 ../../down/station/
下形如 station1.in/out
一共兩祖樣例連結到當前目錄。
第四行是 for 迴圈,\(i\) 從 \(1\) 到 \(2\)。
第五行,首先 time ./station <station$i.in
執行 station,將輸出透過管道傳遞給 diff,與答案檔案進行比較。對於 diff,一共傳遞了四個選項:-b
選項意在忽略行末空格,-B
忽略文末換行(實際上 -Bb
忽略了更多東西,diff --help|grep '\-b'
檢視),-s
選項指出在檔案相同時輸出 ... are identical
,-q
選項指出在檔案不同時僅輸出 ... differ
。本題輸出檔案很大,如果輸出量少,可以去除 -q
選項觀察不同之處。diff 還拿到了兩個檔案,檔案 -
是標準輸入,從管道中獲得;檔案 station$i.out
是樣例輸出。這裡 station$i.out
實際上是個 format string,將 $i
替換成一個數字,寫成 ${i}
也對,這裡變數名只有一個字母,也可以寫 $i
。
第六行結束 for 迴圈。現在你就寫了一個 OI 中可以用的工具指令碼。
寫完以後給他新增執行許可權:chmod u+x test.sh
。然後 ./test.sh
執行。
或者可以去掉第一行的 shebang,使用 bash test.sh
執行,或者 . test.sh
,source test.sh
。保留第一行 shebang,make test
(此時目錄下不要有名為 test 的檔案,包括程式碼),接著 ./test
。首選 chmod
。
執行效果:
$ ls ../../down/station/
station1.in station1.out station2.in station2.out
$ vim test.sh
$ chmod u+x ./test.sh
$ ./test.sh
station.cpp: In function ‘void work(int, int, int, mint)’:
station.cpp:153:10: warning: structured bindings only available with ‘-std=c++17’ or ‘-std=gnu++17’
153 | auto [u, w] = q.front();
| ^
Files - and station1.out are identical
real 0m0.016s
user 0m0.001s
sys 0m0.012s
Files - and station2.out are identical
real 0m0.024s
user 0m0.002s
sys 0m0.017s
花絮:freopen 開關
你用這個 test.sh,需要去掉 freopen。OI 比賽怎麼能隨意去掉 freopen 呢?考慮複製一下檔案:
#!/bin/bash
g++ station.cpp -o station -std=c++14 -O2
for i in {1..2}; do
cp station$i.in station.in
time ./station
diff station.out station$i.out -Bsbq
done
我常用的還有另一種方法是將 freopen 寫成這樣:
#ifndef NF
freopen("station.in", "r", stdin);
freopen("station.out", "w", stdout);
#endif
然後在 g++ 這一行的引數(編譯選項)中加入 -DNF
表示 define 名為 NF
的 macro,以去掉 freopen。之所以名字是 NF,因為 do not finish 的縮寫是 DNF。
對拍器
當前我們有兩份程式碼 main.cpp
與 bf.cpp
,前者因未知原因掛了,後者是樸素程式。寫了一個 dt.py
用於生成一組資料,輸出到標準輸出(可以用 c++ 寫 data.cpp 編譯為可執行檔案,這裡是用 python 寫的)。現在可以進行對拍:
#!/bin/bash
cnt=0
g++ main.cpp -o main -O2 -DNF -g
g++ bf.cpp -o bf -O2 -DNF -g
while :; do
echo Testcase $((++cnt)) is running...
./dt.py > in
./bf <in >ans
./main <in >out
if diff out ans -Bbq; then :
else echo WA; break; fi
done
這裡,:
是一個無效果的命令,始終返回表示”成功執行“的 0。while :;
就是無線迴圈。
\(cnt\) 指示了當前是第幾組資料,應該以 $((++cnt))
的方法使它自增,小心其它方法可能使得 \(cnt\) 變成字串,可以自己嘗試。
第 10 行 if diff out ans -Bbq; then :
這裡比較兩個輸出檔案,相同時返回 0,此時沒有操作。否則,輸出 WA,並中斷迴圈。此時可以檢視輸入檔案 in,輸出檔案 out,答案檔案 ans。去掉 diff 的 -s
是個人喜好,看著加。
備份工具
有時候可能有儲存這場比賽所有程式碼的需求。這樣可以將暴力程式碼留作備份並刪掉,而且檢查程式碼是否編譯透過,還有方便最終提交,最重要的是防止誤刪(曾經試過將一份暴力程式碼 copy 成正解程式碼,導致正解程式碼被覆蓋,無法找回)。
可以編寫 backup.sh
:
#!/bin/bash
tim=$(date|awk -F " " '{print $4}')
dst=$(dirname $0)/tmp/${tim//:/-}
echo start backup at ${tim//:/-}
mkdir $dst
for p in seq butterfly hoshi; do
src=$(dirname $0)/$p/$p.cpp
if [ -e $src ]; then
cp $src $dst
if g++ $src -o $dst/main -O2 -std=c++14; then
echo Problem $p: ok \(remember to check freopen via warnings\)
else
echo Problem $p: CE
fi
else
echo Problem $p: not found
fi
done
rm -f $dst/main
這個有一點長,會有一點難記。可以轉寫成 python 版本,會比較好寫,就是字多一點。下面是逐行解釋:
第一行 shebang。
第二行,提取了當前的時間,data 返回 Thu May 9 21:21:34 CST 2024
這樣一個東西,這東西當然不能直接用作檔名。所以使用 awk -F
分割了這個字串,取出第 4 項(0-indexed),寫法可參考或百度。這時 tim=21:21:34
。
第三行,dst 是 destination,指出備份程式碼存放在 dst 中。$0
是指令碼自己所在的路徑,dirname
取出這個路徑的資料夾名。${tim//:/-}
這裡,因為不能以冒號做檔名,將冒號替換成連字元,和 vim 替換比較像,注意 tim 後面兩個正斜槓表示全部替換。dst
這時就是 ./tmp/21-21-34
,程式碼存放到這個路徑下。
第四行確認一下 tim 是對的,因為有些系統的時間取出來可能是第 3 項、第 5 項,因為中文的原因。自己列舉一下。
第五行新建資料夾。
第六行遍歷 p 為題目名稱,這次比賽有三道題,分別是 seq、butterfly、hoshi。
第七行找出將要備份程式碼的路徑,記為 src,是 source 的縮寫。
第八行,[ -e $src ]
判斷檔案 $src
是否存在,注意兩邊空格不能扔掉。if 判斷之。
第九行,檔案存在,複製到 dst。
第十行,進行編譯測試,只保留 -O2 -std=c++14
這兩個最基本的。
第十一行,編譯透過,輸出資訊。這時如果有 freopen 存在,會對 freopen 報”無返回值“的警告,觀察你的 freopen 對不對。如果沒有就慘了。如果想進一步檢查,可以使用 grep:grep $p $src --color=auto
,看他找不找的到。找不到會無輸出或者只輸出一個,此時你檔名寫錯;找到了有刺眼紅色提醒。對於這個 freopen 檢查,因為傳引數有很多雙引號在,其實是很不好寫的,建議別寫。如果檔名寫對了是很整齊的。
第十三行,發現編譯錯誤,及時提醒。
第十六行,未找到檔案,輸出未找到。
第十九行,清理剛才編譯的可執行檔案。
然後和 test.sh
一樣賦予它執行許可權,然後就可以執行了。
考場上 .vimrc 和 backup.sh 兩個寫完需要 10~20 分鐘,取決於手速,如果發現寫的慢了去讀一下題緩解緊張。
makefile
make 是一個古老的構建工具,幫助我們構建專案。在 OI 中可以幫我們編譯,做到一個一鍵編譯的效果,既有靈活性又少打很多字元。
基礎語法
以下內容都寫在當前目錄下名為 makefile 的檔案中。
目標檔名: 依賴檔案1 依賴檔案2
命令
注意命令前面是一個 TAB。
如:
bf: bf.cpp
g++ bf.cpp -o bf -O2 -DNF -g
main: snow.cpp
g++ snow.cpp -o main -O2 -g -fsanitize=undefined,address -DNF
snow: snow.cpp
g++ snow.cpp -o snow -O2 -g -std=c++14 -DNF -Wall -Wextra -Wconversion
終端輸入 make snow
不是造雪,而是編譯可執行檔案 ./snow
,可以 make snow && ./snow
一個組合技。make bf
也是,make main
也是,main
和 snow
的區別是除錯選項的力度。除錯時使用 main
版本,測樣例使用 snow
版本。優勢在於檔案沒有修改時,不會觸發重新編譯,節省時間。
關於變數:makefile 的變數在每個目標外面,大概這樣寫:
SRC = snow.cpp bf.cpp
和 bash 變數不一樣的是等號兩邊可以加空格,訪問變數是 $(SRC)
圓括號,轉義美元符號是 $$
。這裡面 string 的雙引號規則和 bash 一樣(不用寫)。這個變數其實因為開在全域性,沒得修改,是個常量,所以應該大寫。
$()
圓括號裡面可以呼叫 makefile 的函式,呼叫 bash 函式是 $(shell ls)
這樣子。
傳統題寫法
main: seq.cpp
g++ seq.cpp -o main -DNF -g -O2 -DLOCAL -fsanitize=undefined,address
%: %.cpp
g++ $< -o $@ -DNF -g -std=c++14 -Wall -Wextra -Wconversion -Wshadow
main
就是除錯力度很大的版本,並且 make 呼叫如果沒有任務引數,預設呼叫第一個,可以寫 make && ./main
編譯並執行,make && gdb main -q
編譯並進入除錯狀態。
%.cpp
是一個萬用字元,將 %.cpp
編譯成去掉 .cpp
的可執行檔案,每個匹配到的檔案都是分開的規則。$<
表示第一個依賴檔案(也只有一個),$@
表示目標檔名。然後後面跟著一大堆引數就是編譯警告拉到很滿,可以參考一下,輸出一車警告的,可以仔細看,不看也沒關係。反正真正的編譯檢測是 backup.sh 做的。
這樣以後,對拍器第三、四行可以寫 make -j main bf
(-j
表示兩個編譯任務並行,會快),test.sh 編譯部分改成 make station
,少寫很多的。每個題的 makefile 都不一樣,需要複製幾次。
互動題寫法
main: stub.o toxic.o
g++ $^ -o $@ -fsanitize=undefined,address
%.o: %.cpp toxic.h
g++ $< -o $@ -I. -c -DNF -O2 -g -DLOCAL
這一題提交程式碼是 toxic.cpp
,互動庫是 grader 互動庫 stub.cpp
,有共同標頭檔案 toxic.h
。首先用 g++ -c
將檔案編譯為 .o
但是不連結,引數 -I.
指出標頭檔案在當前目錄(也可以是別的自己寫),後面是常規的編譯選項。構建 main
需要 toxic.o
與 stub.o
,將他們最終連結輸出成可執行檔案。$^
是所有依賴檔案。注意 -fsanitize=undefined,address
寫在連結這一步。
另記:不應該把互動 grader 寫在標頭檔案裡……另外互動題最好把自己的全域性變數扔到 namespace 裡面,防止對面互動庫神操作撞名。
打掃檔案
希望 make clean
刪掉垃圾檔案,包括可執行檔案等。這樣寫:
.PHONY: clean
clean:
rm -f main $(patsubst %.cpp, %, $(wildcard *.cpp))
rm -f $(patsubst %.cpp, %.o, $(wildcard *.cpp))
wildcard *.cpp
是萬用字元匹配,找到所有 .cpp
檔案。patsubst
是字串替換,將 %.cpp
替換成 %
,也就是所有可能的可執行檔名。還有一個 main 也一起刪掉。如果是互動題生成了 .o
也刪掉。
還可以加這四條,把輸入輸出資料刪了(資料剛才說是是軟連結的,所以真實的資料不會有事):
.PHONY: clean
clean:
rm -f main $(patsubst %.cpp, %, $(wildcard *.cpp))
rm -f $(patsubst %.cpp, %.o, $(wildcard *.cpp))
-find . -name '*~'|xargs rm -rf
-find . -name '*in'|xargs rm -rf
-find . -name '*out'|xargs rm -rf
-find . -name '*ans'|xargs rm -rf
.PHONY: clean
表明 clean 是個偽目標,即我們不是真的要生成名為 clean 的檔案。還有另一個常見偽目標 ALL
,這個才是 make 無任務引數呼叫的,預設是第一個而已。
前面有 -
號表示發生錯誤時(如找不到檔案)忽略繼續。rm -f
也會幹這樣的事。
但是在賽場上你是不會想到刪檔案的,所以不用背。
結合 vim
vim 的命令模式也有 :mak[e]
命令(中括號表示可以省略),就是呼叫外面的 make,關鍵是如果編譯錯誤,vim 會將游標定位到第一個出錯的地方,可以 :cn[ext]
到下一個地方。不知道考場上能不能用。