---
# System prepended metadata

title: Shell 高階 1
tags: [Linux, Shell Script, Script]

---

# Shell 高階 1
> 開始看不懂了，不要緊，就讓他過去吧，出事的時候就會看懂了
## Shell
:::spoiler 第一站:嵌入文檔
    * 將資料傳遞給指令
```bash=
COMMAND <<InputComesFromHERE
...
...
...
InputComesFromHERE
```
* 模擬人類的交互操作，下達命令
```bash=
interactive-program <<LimitString
command #1
command #2
...
LimitString
```
* 如果要縮排美觀
    * `<<`後加一個`-`號，這樣bash就會忽略tab
    * 注意這邊的模式不忽略space空格鍵
```bash=
COMMAND <<-InputComesFromHERE
    ...
    ...
    ...
InputComesFromHERE
```

---

* 如果裡面有這些特殊符號，一般來說會展開
* 需要避免展開時可以將限制符包起來：`'...'`
```bash=
COMMAND <<'InputComesFromHERE'
$a `b` \c
#這樣子這些$就不會被展開
InputComesFromHERE
```
* 空命令可以讓程式碼利用Here Documents來註解
```bash=
: <<InputComesFromHERE
註解內容
這邊就不用每行打#了
所以這個也蠻方便的
InputComesFromHERE
```
* 注意結束的限制符不能有空格
```bash=
COMMAND <<InputComesFromHERE
...
...
...
 InputComesFromHERE
# 這裡加了空格，會報錯喔
```
* 最後注意限制符儘量由多的字符組成避免報錯
:::

:::spoiler 第二站:I/O重新定向
1. 使用exec規避子shell
```bash=
#我們今天要讀取檔案並抓取特定行時，會使用grep等管道
cat file.txt | grep apple 
#接下來我們透過設定變數，來使得file中的行加總
count=0
cat file.txt | while read line
do {(( count++ ));} done
#但是這樣的迴圈會產生子shell，縱使在迴圈計數，外部也不會變
#因此我們使用exec的定向，還規避子shell造成的影響
exec 3<>file.txt
while read line <&3; do
  (( count++ ))
done
```
2. 代碼塊的重定向
```bash=
#while,for,until...等，在代碼塊結尾加上“<”進行重定向
until [ "$name" = Smith ]     # 变  !=  为 =.
do
  read name                   # 从 $Filename 读取值, 而不是标准输入.
  echo $name
done <"$Filename"             # 重定向标准输入到文件 "$Filename". 
```
3. 嵌入文件
```bash=
function doesOutput()
 # 当然这也是个外部命令.
 # 这里用函数进行演示会更好一点.
{
  ls -al *.jpg | awk '{print $5,$9}'
}#這邊詳細介紹awk，ls將印出的資訊給到awk，awk依據欄位切割，並將第5&第9欄取出
nr=0          #  我们希望在 'while' 循环里可以操作这些
totalSize=0   #  并且在 'while' 循环结束时看到改变.
while read fileSize fileName ; do#讀取第一個值和第二個值
  echo "$fileName is $fileSize bytes"#印出來
  let nr++
  totalSize=$((totalSize+fileSize))   # Or: "let totalSize+=fileSize"
done<<EOF
$(doesOutput)
EOF
#EOF...EOF這一段最重要，將doesOutput的輸出給while
echo "$nr files totaling $totalSize bytes"
```

---

4. 限制模式
* 在限制模式下運行一個腳本或部分腳本將禁用一些命令，儘管這些命令在正常模式下是可用的。 這是個安全措施，可以限制腳本用戶的許可權，減少運行腳本可能帶來的損害。
* 那些限制：
> 使用 cd 來改變工作目錄。
修改 $PATH， $SHELL， $BASH_ENV 或 $ENV 等環境變數
讀取或修改 $SHELLOPTS，shell環境選項。
輸出重定向。
調用包含 / 的命令。
調用 exec 來替代shell進程。
其他各種會造成混亂或顛覆腳本用途的命令。
在腳本中跳出限制模式。
```bash=
#!/bin/bash
#  在脚本开头用"#!/bin/bash -r"
#+ 可以让整个脚本在限制模式运行。
~
# 到此为止一切都是正常的，非限制模式。
set -r
# set --restricted 效果相同。
~
# 如果想改目錄會跳test.sh: line xx: cd: restricted
# 如果改shell會跳test.sh: line xx: SHELL: readonly variable
```

---

5. 進程替換
* `>(command_list)`：把指令的「輸出」變成一個「可讀檔案」的樣子，供其他程式當作輸入讀取
* `<(command_list)`：把指令的「輸入」變成一個「可寫檔案」，其他程式可以把輸出寫到這裡
```bash=
#wc 的標準輸出格式為：行數 單詞數 字元數 [檔案路徑]
$bash wc <(cat /usr/share/dict/linux.words)
#cat的輸出作為檔案給了wc，因此輸出會像：483523  483523 4992010 /dev/fd/63
$bash grep script /usr/share/dict/linux.words | wc
#因為輸入來自標準輸入(而不是檔案參數)，wc 不會輸出檔案路徑：262 262 3601
```

---

6. 行程取代的一些用法
```bash=
#diff的使用，diff [檔案] [檔案]，diff不支持標準輸入stdin
diff <(command1) <(command2)
#所以我們可以將指令的輸出臨時寫入檔案描述符
diff <(ls -l /bin) <(ls -l /usr)

tar cf >(bzip2 -c > file.tar.bz2) $directory_name
#這邊演示了命令中的進程替換 tar cf [dst] [src]，語意為將src打包成tar給dst
#tar會先把$directory_name打包成tar檔
#接著透過>()打包後的tar檔輸出，變成一個可讀檔案的樣子，給到bzip2
#bzip2用這個tar檔壓縮成file.tar.bz2

#以上的傳統示例
bzip2 -c < pipe > file.tar.bz2& 
#bzip2從pipe這個管道讀取，壓縮成file.tar.bz2，背景執行
tar cf pipe $directory_name
#tar把$directory_name打包成tar並放在pipe，放過去後bzip2就會在背景看到，開始壓縮
rm pipe
#結束後關閉管道pipe

```

---

7. 子shell與管道
```bash=
echo "random input" | while read i
do
  global=3D": Not available outside the loop."
done
echo "\$global (从子进程之外) = $global"
#這個while會在｜的子進程內，因此外部的主進程global不會有值

while read i
do
  echo $i
  global=3D": Available outside the loop."

done < <( echo "random input" )
echo "\$global (使用进程替换) = $global"
#這個while透過進程替換，將echo的output暫時寫入檔案，再交給while執行
#這個while在主進程執行，因此global會改動到！
```

---

* 另一個有趣的？
```bash=
(
route -n | while read x; do ((y++)); done
echo $y # $y is still unset
while read x; do ((y++)); done < <(route -n)
echo $y # $y has the number of lines of output of route -n
)
#這個是子shell中又有子shell
#利用｜的因為while是在子shell中的子shell運行，不會影響到外層的子shell，所以y不會變
#但是用進程替換的while就是在子shell進行，因此相同的，子shell的y會有值
```

---

8. &&和||的使用
* 透過&&讓if等語句少判斷，&&需要每次回傳true才會繼續執行，false終止，節省了許多判斷
```
command-1 && command-2 && command-3 && ... command-n
```
* 透過||與&&語句相反，每次回傳false才會繼續執行，true則終止
```
command-1 || command-2 || command-3 || ... command-n
```
:::

:::spoiler **補給站**:『定向』,『檔案描述符』
* 輸入定向`<`將檔案的內容作為命令的標準輸入
```bash=
command < file
#讓命令從檔案讀取資料，而不是鍵盤
```
* 輸入定向`<<`後續的多行文本作為命令的標準輸入
```bash=
command <<DELIMITER
line 1...
line 2...
DELIMITER
#提供多行輸入給命令，無需外部檔案
```
* 輸出定向`>`將命令的『標準輸出』寫入檔案(覆蓋)
```bash=
command > file
#保存命令輸出到檔案，取代終端顯示
```
* 輸出定向`>>`將命令的『標準輸出』追加至檔案(不覆蓋)
```bash=
command >> file
#累積命令輸出到檔案，適合日誌記錄
```

---

* Bash中的檔案描述符
1. `0`stdin標準輸入
    * 預設連接到鍵盤，用於程式讀取輸入。
2. `1`stdout標準輸出
    * 預設連接到終端，用於程式輸出結果。
3. `2`stderr標準錯誤
    * 預設連接到終端，用於輸出錯誤訊息。

* 修改檔案描述符的作用
    * 讓程式知道該從哪邊接收輸入及輸出
```bash=
exec > file.txt
echo "hi bro"
#這行將hi bro寫到file.txt
exec > 1
#這行有問題，因為 1 已經是標準輸出的檔案描述符，所以這相當於將標準輸出重定向到自身，這可能會導致非預期的行為。
echo "hello bro"
```
```bash=
exec 3>&1       # 保存原本的 stdout 到 FD 3
exec > file.txt
echo "hi bro"   # 寫入到 file.txt
exec >&3        # 還原 stdout
echo "hello bro"  # 印回螢幕
exec 3>&-       # 關閉 FD 3
#重定向標準輸出
```
```bash=
exec 6<&0
exec < data-file 
exec 0<&6 6<&-
#重定向標準輸入
```

---

* 檔案描述符小總結
    * 再將預設的檔案描述符更改前，先將預設的做備份(保存)
    * 當將預設的檔案描述符改回去後，要記得釋放
:::

---

上一站：[Shell 進階](https://hackmd.io/a1Pm_wSlRmaKcmji61DiJg)
下一站：[Shell 高階 2](https://hackmd.io/IaYnG54rT8OzMBOQ1PRJ8g)