跳到主要內容
黯羽輕揚每天積累一點點

語法規則_bash筆記1

免費2017-01-29#Tool#bash入门#bash指南#bash guide#bash语法#bash快速入门

奇怪的語法,嗯,就是奇怪(fi, done, esac)

一. 常識

bash 指令碼基本規則:

執行時展開變數,得到命令和參數字串,執行

一般流程:

# 1.创建/编辑`.sh`文件(道德约束)
vim test.sh
# 2.添可执行权限
chmod +x test.sh
# 3.执行
./test.sh

簡單示例:

#!/bin/bash
# 声明变量
str='hoho'
# 输出变量值
echo $str

其中,第一行 #!/bin/bash 說明直譯器所在路徑,可以透過 which bash 查看

P.S. #! 叫 shebang(釋伴,就這麼翻譯,也這麼讀),更多資訊請查看 釋伴:Linux 上的 Shebang 符號(#!)

二. 變數

1. 環境變數

HOME    # 当前用户目录的绝对路径
USER    # 当前用户名
PWD     # 当前工作目录
# ...
# 更多变量用`env`命令查看

不用宣告,直接使用,類似於 node 裡的 __dirname__filename

建立環境變數的 3 種方式:

  • bashrc 檔案(系統級的 /etc/bashrc 和使用者級的 ~/.bashrc)中添加永久環境變數(每個新建立的 shell 都擁有)

  • 在執行指令碼時設置臨時環境變數(僅在執行指令碼的子 shell 內有效)

  • export 環境變數(只對後續建立的子 shell 有效)

例如:

# 方式1
# 如果是zsh,对应文件名为`~/.zshrc`
echo _ENV=product >> ~/.bashrc
source ~/.bashrc
echo $_ENV

# 方式2
_ENV=product ./test.sh
# 在./test.sh中可以读到_ENV
echo $_ENV

# 方式3
_ENV=product; export _ENV
# 新开一个shell
bash
echo $_ENV

2. 全域變數與區域變數

#!/bin/bash
# 默认声明全局变量
VAR="global variable"
function fn() {
    VAR="updated global variable"
    # 只能在function里通过local关键字声明局部变量
    local VAR="local variable"
    echo $VAR
}
fn
echo $VAR

# 输出
local variable
updated global variable

即用即宣告,預設形式的都是全域變數,只能在函式內部透過 local 關鍵字宣告區域變數。還需要注意

  • 等號兩邊不能有空格,因為每一行會被當作「命令 空格 參數」

  • 引號不是必須的,與 CSS 一樣,內容包含空格時引號才有必要

  • 沒有提升(hoisting)一說,區域變數作用域是從變數宣告位置到函式體結束,全域變數作用域是從宣告位置到檔案結束

3. 存取變數值

$變數名 取變數值,如 $VAR

變數插值規則:在雙引號中引用的變數會被展開(expanded),單引號中的不會,與 PHP 一樣

{} 可以隔離變數名,把變數名保護起來:

${VAR}abc   # VAR的值后面紧跟着字符串abc

存取陣列元素時必須這樣做,例如:

arr=(aa b ccc)
# 输出aa[1],不符合预期
# 因为用$对arr取值,得到aa($arr返回首元),再给后面接上字符串[1]
echo $arr[1]
# 输出b
echo ${arr[1]}

三. 分支與迴圈

1. 條件語句

if 条件      # test命令和[]操作符
then
    语句...
else        # else if写作elif
    语句...
fi

條件部分一般是 test 命令或者 [] 運算子,例如:

if [ $X -lt $Y ];   # X小于Y
if [ -n $X ];       # 变量非空(字符串长度不为0)
if [ -e $path ];    # 文件存在

# 数值比较
if test 2 -gt 1; then echo "number 2 > 1"; fi
# 等价于
if [ 2 -gt 1 ]; then echo "number 2 > 1"; fi

# 字符串比较
if test 2 > 11; then echo "string 2 > 11"; fi
# 等价于
if [ 2 > 11 ]; then echo "string 2 > 11"; fi

其中有 3 個細節,運算元類型、分號和空格:

  • -gt 表示比較數值大於,> 表示比較字串大於,運算子運算時會自動轉換,無法轉換就報錯

  • ; 在單行語句中用來區分塊結構,第一個分號表示條件部分結束,第二個分號表示 then 部分結束,缺一不可

  • 空格用來分隔命令和參數(除了空格,預設分隔符還有定位字元和換行,見下面 IFS)

P.S. 字串轉數值常用方式有 ((str))`expr str`$(expr str),前者是 bash 運算子,後兩個是外部命令

空格示例:

# 空格很关键
if [ 1=2 ];         # 把1=2整体当操作数(字符串)了,没看见操作符
# []里两端的空格也很关键
if [-e $path]; then # 报错[-e命令找不着,因为被看成了'[-e' '$path]'

可以透過 man test 命令查看其它 test 運算子

bash 也提供了類似於 switch 的東西,只是語法很奇怪:

case $variable in
    pattern1)
        command...
        ;;  # break
    pattern2|pattern3)
        command...
        ;;
    patternN)
        command...
        ;;
    *)  # default case
        command...
esac

2. 迴圈語句

有 3 種迴圈:forwhileuntil,語法規則如下:

# for循环
for f in $( ls /var/ ); do
    echo $f
done
# 或者单行的(分号区分结构块)
for f in $( ls /var/ ); do echo $f; done

# while循环
times=6
while [ $times -gt 0 ]; do
    echo Value of times is: $times
    let times=times-1
done
# 单行形式
times=6; while [ $times -gt 0 ]; do echo Value of times is: $times; let times=times-1; done

# until循环
times=0
until [ $times -gt 5 ]; do
    echo Value of times is: $times
    let times=times+1
done

除了 for...in,還有 C 風格的:

arr=(1 '2 3' 4)
len=3
for (( i=0; i<$len; i++)); do
    echo ${arr[${i}]}
done

迴圈的基本規則

迴圈會遍歷由 IFS(' '、'\t'、'\n')分開的項目

IFS(Internal Field Seprator),內部欄位分隔符,預設是空格、tab 和換行,所以注意這種情況:

for f in $( ls -l /var/ ); do echo $f; done

輸出的結果不符合預期

total
0
drwx------
2
root
wheel
68
8
23
2015
agentx

本來應該是這樣:

total 0
drwx------   2 root       wheel        68  8 23  2015 agentx

要迴圈讀整行的話,需要修改 IFS:

# 限制分隔符只认换行
IFS=$'\n'; for f in $( ls -l /var/ ); do echo $f; done

另外,迴圈通常配合萬用字元 * 使用,例如:

# 通配符
echo *      # 当前目录下所有文件/文件夹名,空格分隔
echo *.html # 当前目录下所有html格式文件

# 找test目录下所有html文件
for htmlFile in `echo ~/Documents/projs/test/*.html`; do echo $htmlFile; done
# 找test目录下所有html文件,包括子孙目录
for htmlFile in `echo ~/Documents/projs/test/**/*.html`; do echo $htmlFile; done

迴圈+萬用字元 操作目錄檔案非常方便

四. 函式

1. 函式宣告

function function_name {
    command...
}

# 或者
function_name () {
    command...
}

省略 function 關鍵字的話,函式名稱後面必須要有 (),否則就被當作命令了,例如:

# 报错,parse error near `}'
fn {echo fn}; fn

不省略 function 關鍵字的話,函式名稱後面有沒有 () 無所謂

函式宣告順序不很嚴格,但要保證先宣告後呼叫,例如:

fn1() {echo fn1:`fn2 $1`}
fn2() {echo fn2:$1}

fn1 hoho
# 输出
# fn1:fn2:hoho

如果在宣告 fn2 之前就呼叫 fn1,就會報錯找不到 fn2,如下:

# 报错command not found: fn2
fn1() {echo fn1:`fn2 $1`}; fn1 hoho; fn2() {echo fn2:$1}

2. 呼叫與傳參

參數透過位置變數獲得,不顯式宣告形參:

# 声明函数
fn() {echo $0$1}
# 无参调用
fn
# 传入一个字符串参数hoho
fn hoho

函式作用域內,提供了一些位置變數(都是唯讀的):

$0  # 函数名(不算参数,因为$*和$@不包含$0,$#也不计$0)
$n  # 第n个参数,参数从1开始
$*  # 由所有参数拼成的字符串,用空格分隔
$@  # 同上,区别是每个参数会被双引号保护起来
$#  # 参数个数

P.S. 注意 $10 不是 ${10},前者是 $1 後面跟個字串 0,後者是第 10 個參數的值

另外,這些位置變數也適用於透過命令列向指令碼傳參,例如:

# sub.sh
echo $1-$2=`expr $1 - $2`

# 命令行执行
./sum.sh 1 2
# 输出
# 1-2=-1

$*$@ 的區別很重要,簡單理解:

$*=$1 $2 $3...
$@="$1" "$2" "$3"...

示例:

# 没有区别
fn1() {for arg in $*; do echo line:$arg; done}; fn1 "a" "b c" "d"
fn2() {for arg in $@; do echo line:$arg; done}; fn2 "a" "b c" "d"
# 输出
# line:a
# line:b c
# line:d

# 用双引号包起来时能发现区别
fn1() {for arg in "$*"; do echo line:$arg; done}; fn1 "a" "b c" "d"
# 输出
# line:a
# b c
# d
fn2() {for arg in "$@"; do echo line:$arg; done}; fn2 "a" "b c" "d"
# 输出
# line:a
# line:b c
# line:d

用雙引號包起來時,迴圈次數有差異,$* 只迴圈一次,$@ 迴圈 3 次,所以一般建議使用 $@

3. 回傳值

3 種方式都不好用,沒事就不要回傳了(直接修改外部變數),非要回傳值的話,建議用子 shell 執行,echo 傳回的方式,但注意事項(見程式碼註釋)也很麻煩,示例如下:

# 1.return
fn() {
    return -2
}
fn
# 取出上一条命令的返回值,0表示正常,非0不正常
# 实际输出是254,超出范围的会被框进来,256会变成0,-1变成255
echo $?

# 缺点:return表示函数执行状态,只能返回[0, 255]的整数,无法return字符串
#      其次`$?`必须紧跟在函数调用后面


# 2.子shell执行,echo传回
fn() {
    echo -2
    # 避免错误信息进入标准输出,直接丢掉
    cat xxx 2> /dev/null
}

echo $(fn)

# 缺点:给标准输出的结果可能不干净
#      (函数体不止一条`echo`语句,或者有`print`、`printf`之类的也输出到标准输出的语句)
#      另外,如果函数执行过程中出错了,错误信息也会混进去(虽然可以避免,但比较麻烦)

# 3.所谓的传引用
fn() {
    # 约定第一个参数传入的字符串是返回变量名
    local res=$1
    # 计算1+2,再按照返回变量名创建全局变量,带回结果
    eval $res=$(($2 + $3))
}
   
fn result 1 2
echo $result

# 缺点:其实就是全局变量传值,只是全局变量名动态传入,没有在函数里写死而已

五. 陣列

1. 宣告與賦值

# 空数组
arr=()
# 字符串数组,空格分隔元素,不用逗号
arr=(1 2 3 'we together')

# 直接赋值,没有就新增一个
arr[0]=4
arr[6]='sixth'

陣列索引從 0 開始,賦值時不需要保證連續,沒有的話會新增一個

2. 遍歷

for 迴圈不用知道陣列長度:

arr=(1 2 3 '4 5')
for i in "${arr[@]}"; do echo $i; done

特別注意 "${arr[@]}",與函式裡的位置變數類似,$*$@ 的迴圈次數不同:

for i in "${arr[@]}"; do echo $i; done
# 输出
# 1
# 2
# 3
# 4 5

for i in "${arr[*]}"; do echo $i; done
# 输出
# 1 2 3 4 5

while 和 until 需要知道陣列長度:

# 取数组长度
len=${#arr[@]}
i=1
while [ $i -lt $len ]; do echo $arr[$i]; i=$((i+1)); done
# 或者until
until [ $len -lt 1 ]; do len=$((len-1)); echo ${arr[$len]}; done

注意${#str} 是取字串長度,與 ${#arr[@]} 取陣列長度很像,容易弄錯

六. 命令替換

命令替換是指在 bash 指令碼中執行 shell 命令,並得到其輸出結果(差不多是這意思,沒有找到嚴格定義)

直接執行的話,結果會被輸出到標準輸出(螢幕),想把結果取出來的話,就需要用到命令替換:

# 直接执行 屏幕输出了ls命令的结果
ls
# 命令替换 屏幕不输出结果,由自定义变量记下
lsResult=`ls`

# 看起来像是丢弃结果(不输出也不记)
# 实际上可能会报错,`ls`命令返回结果字符串,会被当作命令继续执行
`ls`

命令替換常用的方式有 2 種,反撇號擴充和圓括號擴充,例如:

# 反撇号扩展,不允许嵌套
files=`ls`
# 圆括号扩展,允许嵌套
files="$(ls)"
# 嵌套示例
# 当前目录下文件及文件夹数量+1
$(expr ${#$(ls)[@]} + 1)

有趣 的一點:兩種命令替換方式都是新建 shell(也就是在子 shell 中)執行命令,不會對目前 shell 產生任何影響,所以可以方便的隔離操作環境,例如:

# 去新环境执行cd
lsParent="$(cd ../; ls)"
# 执行后pwd不变,不用再cd回来

到這裡基本足夠完成稍複雜的(有用的)bash 指令碼了

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論