r3f.cn
GitHub Repo stars

Bash

这是 Linux Bash shell 脚本入门的快速参考备忘单。

#入门

#hello.sh

#!/bin/bash

VAR="world"
echo "Hello $VAR!" # => Hello world!

执行脚本

$ bash hello.sh

#变量

NAME="John"

echo ${NAME}    # => John (变量)
echo $NAME      # => John (变量)
echo "$NAME"    # => John (变量)
echo '$NAME'    # => $NAME (精确字符串)
echo "${NAME}!" # => John! (变量)

NAME = "John"   # => 错误 (关于空格)

#注释

# 这是 Bash 的行内注释。
: '
这是一个
非常简洁的注释
在 bash 中
'

多行注释使用 :' 开始,' 结束

#参数

表达式 描述
$1$9 参数 1 ... 9
$0 脚本本身的名称
$1 第一个参数
${10} 位置参数 10
$# 参数数量
$$ shell 的进程 ID
$* 所有参数
$@ 所有参数,从第一个开始
$- 当前选项
$_ 上一个命令的最后一个参数

参见:特殊参数

#函数

get_name() {
        echo "John"
}

echo "You are $(get_name)"

参见:函数

#条件语句

if [[ -z "$string" ]]; then
        echo "字符串为空"
elif [[ -n "$string" ]]; then
        echo "字符串不为空"
fi

参见:条件语句

#大括号展开

echo {A,B}.js

表达式 描述
{A,B} 等同于 A B
{A,B}.js 等同于 A.js B.js
{1..5} 等同于 1 2 3 4 5

参见:大括号展开

#Shell 执行

# => I'm in /path/of/current
echo "I'm in $(PWD)"

# 等同于:
echo "I'm in `pwd`"

参见:命令替换

#Bash 参数展开

#语法

代码 描述
${FOO%suffix} 移除后缀
${FOO#prefix} 移除前缀
${FOO%%suffix} 移除长后缀
${FOO##prefix} 移除长前缀
${FOO/from/to} 替换首次匹配
${FOO//from/to} 全部替换
${FOO/%from/to} 替换后缀
${FOO/#from/to} 替换前缀

#子字符串

表达式 描述
${FOO:0:3} 子字符串 (位置, 长度)
${FOO:(-3):3} 从右侧开始的子字符串

#长度

表达式 描述
${#FOO} $FOO 的长度

#默认值

表达式 描述
${FOO:-val} $FOO,如果未设置则为 val
${FOO:=val} 如果未设置,则将 $FOO 设置为 val
${FOO:+val} 如果 $FOO 已设置,则为 val
${FOO:?message} 如果 $FOO 未设置,则显示消息并退出

#替换

echo ${food:-Cake}  #=> $food 或 "Cake"
STR="/path/to/foo.cpp"
echo ${STR%.cpp}    # /path/to/foo
echo ${STR%.cpp}.o  # /path/to/foo.o
echo ${STR%/*}      # /path/to

echo ${STR##*.}     # cpp (扩展名)
echo ${STR##*/}     # foo.cpp (基本路径)

echo ${STR#*/}      # path/to/foo.cpp
echo ${STR##*/}     # foo.cpp

echo ${STR/foo/bar} # /path/to/bar.cpp

#切片

name="John"
echo ${name}           # => John
echo ${name:0:2}       # => Jo
echo ${name::2}        # => Jo
echo ${name::-1}       # => Joh
echo ${name:(-1)}      # => n
echo ${name:(-2)}      # => hn
echo ${name:(-2):2}    # => hn

length=2
echo ${name:0:length}  # => Jo

参见:参数展开

#基本路径和目录路径

SRC="/path/to/foo.cpp"
BASEPATH=${SRC##*/}
echo $BASEPATH  # => "foo.cpp"


DIRPATH=${SRC%$BASEPATH}
echo $DIRPATH   # => "/path/to/"

#转换

STR="HELLO WORLD!"
echo ${STR,}   # => hELLO WORLD!
echo ${STR,,}  # => hello world!

STR="hello world!"
echo ${STR^}   # => Hello world!
echo ${STR^^}  # => HELLO WORLD!

ARR=(hello World)
echo "${ARR[@],}" # => hello world
echo "${ARR[@]^}" # => Hello World

#Bash 数组

#定义数组

Fruits=('Apple' 'Banana' 'Orange')

Fruits[0]="Apple"
Fruits[1]="Banana"
Fruits[2]="Orange"

ARRAY1=(foo{1..2}) # => foo1 foo2
ARRAY2=({A..D})    # => A B C D

# 合并 => foo1 foo2 A B C D
ARRAY3=(${ARRAY1[@]} ${ARRAY2[@]})

# declare 结构
declare -a Numbers=(1 2 3)
Numbers+=(4 5) # 追加 => 1 2 3 4 5

#索引

- -
${Fruits[0]} 第一个元素
${Fruits[-1]} 最后一个元素
${Fruits[*]} 所有元素
${Fruits[@]} 所有元素
${#Fruits[@]} 元素总数
${#Fruits} 第一个元素的长度
${#Fruits[3]} 第 n 个元素的长度
${Fruits[@]:3:2} 范围
${!Fruits[@]} 所有键

#迭代

Fruits=('Apple' 'Banana' 'Orange')

for e in "${Fruits[@]}"; do
        echo $e
done

#带索引

for i in "${!Fruits[@]}"; do
    printf "%s\t%s\n" "$i" "${Fruits[$i]}"
done

#操作

Fruits=("${Fruits[@]}" "Watermelon")     # 推入 (Push)
Fruits+=('Watermelon')                   # 也可推入 (Push)
Fruits=( ${Fruits[@]/Ap*/} )             # 通过正则表达式匹配移除
unset Fruits[2]                          # 移除一个项目
Fruits=("${Fruits[@]}")                  # 复制
Fruits=("${Fruits[@]}" "${Veggies[@]}")  # 连接
lines=(`cat "logfile"`)                  # 从文件读取

#数组作为参数

function extract()
{
        local -n myarray=$1
        local idx=$2
        echo "${myarray[$idx]}"
}
Fruits=('Apple' 'Banana' 'Orange')
extract Fruits 2     # => Orange

#Bash 字典

#定义

declare -A sounds
sounds[dog]="bark"
sounds[cow]="moo"
sounds[bird]="tweet"
sounds[wolf]="howl"

#使用字典

echo ${sounds[dog]} # 狗的声音
echo ${sounds[@]}   # 所有值
echo ${!sounds[@]}  # 所有键
echo ${#sounds[@]}  # 元素数量
unset sounds[dog]   # 删除狗

#迭代

for val in "${sounds[@]}"; do
        echo $val
done

for key in "${!sounds[@]}"; do
        echo $key
done

#Bash 条件语句

#整数条件

条件 描述
[[ NUM -eq NUM ]]
[[ NUM -ne NUM ]]
[[ NUM -lt NUM ]]
[[ NUM -le NUM ]] 于或
[[ NUM -gt NUM ]]
[[ NUM -ge NUM ]] 于或
(( NUM < NUM )) 小于
(( NUM <= NUM )) 小于或等于
(( NUM > NUM )) 大于
(( NUM >= NUM )) 大于或等于

#字符串条件

条件 描述
[[ -z STR ]] 空字符串
[[ -n STR ]] 空字符串
[[ STR == STR ]] 相等
[[ STR = STR ]] 相等 (同上)
[[ STR < STR ]] 小于 (ASCII)
[[ STR > STR ]] 大于 (ASCII)
[[ STR != STR ]] 不相等
[[ STR =~ STR ]] 正则表达式

#示例

#字符串

if [[ -z "$string" ]]; then
        echo "字符串为空"
elif [[ -n "$string" ]]; then
        echo "字符串不为空"
else
        echo "这永远不会发生"
fi

#组合

if [[ X && Y ]]; then
        ...
fi

#相等

if [[ "$A" == "$B" ]]; then
        ...
fi

#正则表达式

if [[ '1. abc' =~ ([a-z]+) ]]; then
        echo ${BASH_REMATCH[1]}
fi

#更小

if (( $a < $b )); then
     echo "$a 小于 $b"
fi

#存在

if [[ -e "file.txt" ]]; then
        echo "文件存在"
fi

#文件条件

条件 描述
[[ -e FILE ]]
[[ -d FILE ]]
[[ -f FILE ]]
[[ -h FILE ]] 符号链接
[[ -s FILE ]] 大小 > 0 字节
[[ -r FILE ]]
[[ -w FILE ]]
[[ -x FILE ]] 可执行
[[ f1 -nt f2 ]] f1 f2
[[ f1 -ot f2 ]] f2 f1
[[ f1 -ef f2 ]] 相同文件

#更多条件

条件 描述
[[ -o noclobber ]] 如果选项已启用
[[ ! EXPR ]]
[[ X && Y ]]
[[ X | | Y ]]

#逻辑与、或

if [ "$1" = 'y' -a $2 -gt 0 ]; then
        echo "yes"
fi

if [ "$1" = 'n' -o $2 -lt 0 ]; then
        echo "no"
fi

#Bash 循环

#基本 for 循环

for i in /etc/rc.*; do
        echo $i
done

#C 风格 for 循环

for ((i = 0 ; i < 100 ; i++)); do
        echo $i
done

#范围

for i in {1..5}; do
        echo "Welcome $i"
done

#带步长

for i in {5..50..5}; do
        echo "Welcome $i"
done

#Continue

for number in $(seq 1 3); do
        if [[ $number == 2 ]]; then
                continue;
        fi
        echo "$number"
done

#Break

for number in $(seq 1 3); do
        if [[ $number == 2 ]]; then
                # 跳过循环的其余部分。
                break;
        fi
        # 这只会打印 1
        echo "$number"
done

#Until

count=0
until [ $count -gt 10 ]; do
        echo "$count"
        ((count++))
done

#While 与递增

i=1
while [[ $i -lt 4 ]]; do
        echo "Number: $i"
        ((i++))
done

#While 与递减

i=3
while [[ $i -gt 0 ]]; do
        echo "Number: $i"
        ((i--))
done

#与 test 结合

i=3
while ((i--)); do
        echo "Number: $i"
done

#无限循环

while true; do
        # 这里是一些代码。
done

#无限循环 (简写)

while :; do
        # 这里是一些代码。
done

#读取行

cat file.txt | while read line; do
        echo $line
done

#Bash 函数

#定义函数

myfunc() {
        echo "hello $1"
}
# 与上面相同 (备用语法)
function myfunc() {
        echo "hello $1"
}
myfunc "John"

#返回值

myfunc() {
        local myresult='some value'
        echo $myresult
}
result="$(myfunc)"

#抛出错误

myfunc() {
        return 1
}
if myfunc; then
        echo "success"
else
        echo "failure"
fi

#Bash 选项

#选项

# 避免覆盖文件
# (echo "hi" > foo)
set -o noclobber

# 用于在出错时退出
# 避免级联错误
set -o errexit

# 揭示隐藏的失败
set -o pipefail

# 暴露未设置的变量
set -o nounset

#Glob 选项

# 不匹配的 glob 被移除
# ('*.foo' => '')
shopt -s nullglob

# 不匹配的 glob 抛出错误
shopt -s failglob

# 不区分大小写的 glob
shopt -s nocaseglob

# 通配符匹配点文件
# ("*.sh" => ".foo.sh")
shopt -s dotglob

# 允许 ** 进行递归匹配
# ('lib/**/*.rb' => 'lib/a/b/c.rb')
shopt -s globstar

#Bash 历史

#命令

命令 描述
history 显示历史
sudo !! 使用 sudo 运行上一个命令
shopt -s histverify 不要立即执行展开的结果

#展开

表达式 描述
!$ 展开最近命令的最后一个参数
!* 展开最近命令的所有参数
!-n 展开第 n 个最近的命令
!n 展开历史中的第 n 个命令
!<command> 展开命令 <command> 的最近一次调用

#操作

代码 描述
!! 再次执行上一个命令
!!:s/<FROM>/<TO>/ 在最近的命令中将 <FROM> 的首次出现替换为 <TO>
!!:gs/<FROM>/<TO>/ 在最近的命令中将 <FROM> 的所有出现替换为 <TO>
!$:t 仅从最近命令的最后一个参数展开基本名称
!$:h 仅从最近命令的最后一个参数展开目录

!!!$ 可以替换为任何有效的展开。

#切片

代码 描述
!!:n 仅从最近的命令展开第 n 个标记 (命令是 0;第一个参数是 1)
!^ 从最近的命令展开第一个参数
!$ 从最近的命令展开最后一个标记
!!:n-m 从最近的命令展开标记范围
!!:n-$ 从最近的命令展开第 n 个标记到最后一个标记

!! 可以替换为任何有效的展开,例如 !cat!-2!42 等。

#其他

#数值计算

$((a + 200))      # 将 $a 增加 200
$(($RANDOM%200))  # 随机数 0..199

#子 Shell

(cd somedir; echo "我现在在 $PWD")
pwd # 仍然在第一个目录

#检查命令

command -V cd
#=> "cd 是一个函数/别名/等等"

#重定向

python hello.py > output.txt   # stdout 到 (文件)
python hello.py >> output.txt  # stdout 到 (文件), 追加
python hello.py 2> error.log   # stderr 到 (文件)
python hello.py 2>&1           # stderr 到 stdout
python hello.py 2>/dev/null    # stderr 到 (空设备)
python hello.py &>/dev/null    # stdout 和 stderr 到 (空设备)
python hello.py < foo.txt      # 将 foo.txt 提供给 python 的 stdin

#相对路径 Source

source "${0%/*}/../share/foo.sh"

#脚本目录

DIR="${0%/*}"

#Case/Switch 语句

case "$1" in
        start | up)
        vagrant up
        ;;

        *)
        echo "用法: $0 {start|stop|ssh}"
        ;;
esac

#捕获错误

trap 'echo 大约在 $LINENO 行发生错误' ERR

traperr() {
        echo "错误: ${BASH_SOURCE[1]} 大约在 ${BASH_LINENO[0]} 行"
}

set -o errtrace
trap traperr ERR

#printf

printf "Hello %s, I'm %s" Sven Olga
#=> "Hello Sven, I'm Olga

printf "1 + 1 = %d" 2
#=> "1 + 1 = 2"

printf "Print a float: %f" 2
#=> "Print a float: 2.000000"

#获取选项

while [[ "$1" =~ ^- && ! "$1" == "--" ]]; do case $1 in
        -V | --version )
        echo $version
        exit
        ;;
        -s | --string )
        shift; string=$1
        ;;
        -f | --flag )
        flag=1
        ;;
esac; shift; done
if [[ "$1" == '--' ]]; then shift; fi

#检查命令结果

if ping -c 1 google.com; then
        echo "看起来你有可用的网络连接"
fi

#特殊变量

表达式 描述
$? 上一个任务的退出状态
$! 上一个后台任务的 PID
$$ Shell 的 PID
$0 Shell 脚本的文件名

参见 特殊参数

#Grep 检查

if grep -q 'foo' ~/.bash_history; then
        echo "你过去似乎输入过 'foo'"
fi

#反斜杠转义

  •  
  • !
  • "
  • #
  • &
  • '
  • (
  • )
  • ,
  • ;
  • <
  • >
  • [
  • |
  • \
  • ]
  • ^
  • {
  • }
  • `
  • $
  • *
  • ?

使用 \ 转义这些特殊字符

#Heredoc (在此文档)

cat <<END
hello world
END

#返回上一个目录

pwd # /home/user/foo
cd bar/
pwd # /home/user/foo/bar
cd -
pwd # /home/user/foo

#读取输入

echo -n "继续吗? [y/n]: "
read ans
echo $ans
read -n 1 ans    # 仅一个字符

#条件执行

git commit && git push
git commit || echo "提交失败"

#严格模式

set -euo pipefail
IFS=$'\n\t'

参见:非官方 bash 严格模式

#可选参数

args=("$@")
args+=(foo)
args+=(bar)
echo "${args[@]}"

将参数放入数组然后追加

#另请参阅