- Shell脚本编程30分钟入门 解答了我多年的疑惑,比如如何运行 shell 脚本。 test.sh 不行 ./test.sh 才行
- Bash 脚本教程 --阮一峰
- 作为可执行程序
文件的第一行需要有 Shebang。
chmod +x ./test.sh ./test.sh
以上三种写法都可。#!/bin/sh #!/bin/bash #!/usr/bin/env bash
#!/usr/bin/env NAME
这个语法的意思是,让 Shell 查找$PATH环境变量里面第一个匹配的NAME。如果你不知道某个命令的具体路径,或者希望兼容其他用户的机器,这样的写法就很有用。如 Node.js 脚本可以写成#!/usr/bin/env node
- 作为解释器的参数运行
sh ./test.sh
# 定义变量
name="hello" # 注意=号前后不能有空格
echo $name
echo ${name} # 变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,最佳实践是总是给变量加上花括号
# 除了显式地直接赋值,还可以用语句给变量赋值,如:
file=`ls -l .`
echo ${file}
file2=$(ls -l .)
echo ${file2}
# 变量重新赋值
name="world"
echo ${name}
# 删除变量
unset name
echo ${name}
注意:shell中 ``
、$()
、${}
这三个符号的区别
$( )
与``
(反引号)都是用来作命令替换的(大白话就是命令的拼接)。
命令替换与变量替换差不多,都是用来重组命令行的,先完成引号里的命令行,然后将其结果替换出来,再重组成新的命令行。
# 下面两种写法等价的。单推荐用$(),因为``很容易与''搞混乱,尤其对初学者来说,而$( )比较直观。
echo today is $(date "+%Y-%m-%d")
echo today is `date "+%Y-%m-%d"`
# ${}则是用来分隔变量的
A=hello
echo $AB # 输出空,因为 变量 AB 不存在
echo ${A}B # helloB
Bash 提供四个特殊语法,跟变量的默认值有关,目的是保证变量不为空。
${val:-word} # val存在且不为空,返回val,否则返回word
${val:=word} # val存在且不为空,返回val,否则将word赋值给val并返回word
${val:+word} # val存在且不为空,返回word,否则返回空值,目的是测试变量是否存在,比如${count:+1}表示变量count存在时返回1
${val:?message} # val存在且不为空,返回val,否则打印出message错误信息,message不传则打印默认信息
- 单引号:
- 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的
- 单引号字串中不能出现单引号(对单引号使用转义符后也不行)
- 双引号
- 双引号里可以有变量
- 双引号里可以出现转义字符
name="bmxkl"
# str1='Hello, \'${name}\'' # 报错:
str1='Hello, \"${name}\"'
echo ${str1} # 输出:Hello, \"${name}\"
str2="Hello, \"${name}\""
echo ${str2} # 输出:Hello, "bmxkl"
# 拼接字符串
name="bmxkl"
str1="Hello, "$name" !"
str2="Hello, "${name}" !"
echo ${str1} ${str2} # 输出:Hello, bmxkl ! Hello, bmxkl !
# 获取字符串长度
echo ${#name} # 输出:5
# 提取子字符串 ${varname:offset:length}
str="hello world"
echo ${str:3:5} # 两个参数分别是 起始位置,截断长度。输出:lo wo
# 任意位置的模式匹配。
# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,但仅替换第一个匹配
${variable/pattern/string}
# 如果 pattern 匹配变量 variable 的一部分,
# 最长匹配(贪婪匹配)的那部分被 string 替换,所有匹配都替换
${variable//pattern/string}
# 模式必须出现在字符串的开头
${variable/#pattern/string}
# 如果 pattern 匹配变量 variable 的开头,
# 删除最短匹配(非贪婪匹配)的部分,返回剩余部分
${variable#pattern}
# 如果 pattern 匹配变量 variable 的开头,
# 删除最长匹配(贪婪匹配)的部分,返回剩余部分
${variable##pattern}
# 模式必须出现在字符串的结尾
${variable/%pattern/string}
# 1. 字符串头部的模式匹配
myPath=/home/cam/book/long.file.name
echo ${myPath#/*/} # cam/book/long.file.name
echo ${myPath##/*/} # long.file.name
# 替换
echo ${myPath/#*\//test_} # test_long.file.name
# 下面的效果一样,都输出:hello world
echo hello world
echo "hello world"
echo 'hello world'
# 输出多行
echo "<HTML>
<HEAD>
<TITLE>Page Title</TITLE>
</HEAD>
<BODY>
Page body.
</BODY>
</HTML>"
# -n 参数:默认换行,加上 -n 不换行
# 输出:
# a
# b
echo a;echo b;
# 输出
# ab
echo -n a;echo b
# -e 参数:特殊字符默认原样输出,加上后会转义
# hello\nworld
echo "hello\nworld";
# hello
# world
echo -e "hello\nworld";
# 用空格分隔参数,如果参数之间有多个空格,Bash 会自动忽略多余的空格。
echo this is a test; # this is a test
# ;是命令的结束符,使用分号时,第二个命令总是接着第一个命令执行,不管第一个命令执行成功或失败。
cat notexist.txt; echo hello; # 输出hello
# 命令的组合符&&和||
# && 前一个命令成功才执行后一个命令
cat notexist.txt && echo hello; # 不输出hello
# || 前一个命令失败才执行后一个命令
cat notexist.txt || echo hello; # 不输出hello
pwd || echo hello; # 不输出hello
# type: Bash 本身内置了很多命令,同时也可以执行外部程序。怎么知道一个命令是内置命令,还是外部程序呢?
type ls # ls is /bin/ls 说明 ls 是外部程序
type echo # echo is a shell builtin 说明 type 是内置命令
# 输出:
# 4
# 3
# 2
# 1
for i in {4..1}
do
echo $i
done
# 输出:2007-1 2007-2 2007-3 2007-4 2007-5 2007-6 2007-7 2007-8 2007-9 2007-10 2007-11 2007-12 2008-1 2008-2 2008-3 2008-4 2008-5 2008-6 2008-7 2008-8 2008-9 2008-10 2008-11 2008-12 2009-1 2009-2 2009-3 2009-4 2009-5 2009-6 2009-7 2009-8 2009-9 2009-10 2009-11 2009-12
echo {2007..2009}-{01..12}
echo $date # 输出空,因为$date 是个未定义的变量
echo \$date # 输出:$date
echo "a\tb"
echo -e "a\tb"
# 一行命令写成多行
# 换行符是一个特殊字符,表示命令的结束,Bash 收到这个字符以后,就会对输入的命令进行解释执行。换行符前面加上反斜杠转义,就使得换行符变成一个普通字符,Bash 会将其当作长度为0的空字符处理,从而可以将一行命令写成多行。
echo hello \
world \
bash \
# $? 为上一个命令的退出码,用来判断上一个命令是否执行成功。返回值是0,表示上一个命令执行成功;如果不是零,表示上一个命令执行失败。
cat notexist.txt;
echo $? # 输出: 1
# $$ 当前 shell 的进程 id
echo $$
echo log_$$.txt # log_58566.txt
# $_ 上一个命令的最后一个参数
ls -l /root;
echo $_; # /root
# $0 为当前 Shell 的名称(在命令行直接执行时)或者脚本名(在脚本中执行时)
echo $0; # ./test.sh
# $#表示脚本的参数数量,$@表示脚本的参数值
# 执行:./test.sh a b c 输出为: 3 a b c
echo $# $@;
-
算术表达式 如果要读取算术运算的结果,需要在
((...))
前面加上美元符号$((...))
,使其变成算术表达式,返回算术运算的值。echo $((2 + 2)) # 4 +号前后可以有空格,也可以没有
-
expr 命令
expr 3 + 2 # 5 注意3 + 2中间要有空格
-
let 命令:let命令用于将算术运算的结果,赋予一个变量。
let x=3+3 # 注意:x=3+3 中间不能有空格,=和+前后都不能有空格
从上面三个例子也能看出 shell 的语法有多么的怪异,分别是允许空格,必须有空格,不能有空格。令人抓狂啊!
详见: https://wangdoc.com/bash/history 常用的就是
history # 查看历史命令
history | grep echo # 配合管道符和grep搜索
script.sh word1 word2 word3
脚本文件内部,可以使用特殊变量,引用这些参数。
$0:脚本文件名,即script.sh。
$1~$9:对应脚本的第一个参数到第九个参数。第10个参数使用${10}
$#:参数的总数。
$@:全部的参数,参数之间使用空格分隔。
$*:全部的参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。
# ./test.sh 1 2 3 4
# 全部参数: 1 2 3 4
# 参数数量: 4
# $0 = test.sh
# $1 = 1
# $2 = 2
# $3 = 3
# $4 = 4
echo '全部参数:' $@
echo '参数数量:' $#
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3
# 输入任意数量的参数,利用for循环,可以读取每一个参数。
for i in "$@"; do
echo $i
done
source命令用于执行一个脚本,通常用于重新加载一个配置文件。
$ source .bashrc
source命令最大的特点是在当前 Shell 执行脚本,不像直接执行脚本时,会新建一个子 Shell。所以,source命令执行脚本时,不需要export变量。
# source_test.sh 文件内容如下
#!/bin/bash
echo $foo
在bash分别执行
foo=1
source source_test.sh
. source_test.sh
输出1
bash source_test.sh
则输出空字符串
alias命令用来为一个命令指定别名,这样更便于记忆。下面是设置alias的格式。
alias NAME=DEFINITION
查看 alias
alias # 查看所有 alias
alias l # 查看指定命令的 alias
# alias l
# l='ls -lah'
比如 mac 的oh-my-zsh 就配置了很多实用的 git alias,很好用,我也参考这在 window 的git bash里面配了一些 alias。
read命令的格式如下。
read [-options] [variable...]
上面语法中,options是参数选项,variable是用来保存输入数值的一个或多个变量名。如果没有提供变量名,环境变量 REPLY
会包含用户输入的一整行数据。
echo -n "输出文本 >"
read text
echo "你的输入 ${text}"
echo Please, enter your firstname and lastname
read fn ln
echo "hi $fn $ln"
read的参数啊
-t
超时参数-p
提示信息-s
用户输入不展示在屏幕read -sp "enter you name >" name echo "Your name: $name"
- ...
语法
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
if 关键字后面也可以是一条命令,如果命令成功(返回值为0),则判断条件成立
if echo 'hi'; then echo "hello world";fi;
# hi
# hello world
if 后面可以跟任意数量的命令,所有命令都会执行,但只要最后一个命令执行成功(返回值为0),则会执行 then 的部分
# if 的最后一个命令执行成功
if ls a.txt; ls; then echo "hello world";fi;
# ls: a.txt: No such file or directory
# source_test.sh test.sh
# hello world
# if 的最后一个命令执行失败
if ls a.txt; ls b.txt; then echo "hello world";fi;
# ls: a.txt: No such file or directory
# ls: b.txt: No such file or directory
if false; true; then echo "hello world";fi; # hello world
elif 部分可以有多个
read -p "input [1~3] >" num
if [ "$num" = "1" ]; then
echo 1
elif [ "$num" = "2" ]; then
echo 2
elif [ "$num" = "3" ]; then
echo 3
else
echo "invalid"
fi
if 的判断条件,一般使用 test 命令,有三种形式
test expression
[ expression ]
[[ expression ]]
expression 是一个表达式,表达式为真,test命令执行成功(返回值为0);表达式为假,test命令执行失败(返回值为1)。[
相当于是 test
的简写
三种形式等价,但最后一种支持正则。
注意:后面两种[]
和表达式之间必须有空格。
下面三种写法都是判断一个文件是否存在:
if test -f /etc/hosts; then
echo 'file exists';
fi;
if [ -f /etc/hosts ]; then
echo 'file exists';
fi;
if [[ -f /etc/hosts ]]; then
echo 'file exists';
fi;
[ -a file ] 如果 file 存在,则为true
[ -e file ] 如果 file 存在,则为true
[ -f file ] 如果 file 存在且是一个普通文件,则为true
[ -d file ] 如果 file 存在并且是一个目录,则为true
[ -r file ]:如果 file 存在并且可读(当前用户有可读权限),则为true。
[ -w file ]:如果 file 存在并且可写(当前用户有可写权限),则为true。
[ -x file ]:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为true。
read -p "input a file: " file;
if [ -e $file ]; then
if [ -f $file ]; then
echo "$file is regular file";
fi
if [ -d $file ]; then
echo "$file is directory";
fi
if [ -r $file ]; then
echo "$file is readable";
fi
if [ -w $file ]; then
echo "$file is writable";
fi
if [ -x $file ]; then
echo "$file is exectable";
fi
else
echo "$file not exist";
exit 1
fi
注意: 上面程序有bug,如果不输入文件直接回车则会输出:
input a file:
is regular file
is directory
is readable
is writable
is exectable
因为如果 $file
为空则 [ -e $file ]
会变为 [ -e ]
,会判断为真,返回值为0;而如果$file
放在双引号中,[ -e $file ]
会变为 [ -e "" ]
,会判断为假,返回值为1
[ -e ]
echo $? # 0
[ -e "" ];
echo $? # 1
[ -e xx.sh ]
echo $? # 1
正确做法应该是: if [ -r "$file" ]
read -p "input a file: " file;
if [ -e "$file" ]; then
if [ -f "$file" ]; then
echo "$file is regular file";
fi
if [ -d "$file" ]; then
echo "$file is directory";
fi
if [ -r "$file" ]; then
echo "$file is readable";
fi
if [ -w "$file" ]; then
echo "$file is writable";
fi
if [ -x "$file" ]; then
echo "$file is exectable";
fi
else
echo "$file not exist";
exit 1
fi
直接回车则正确,会输出
input a file:
not exist
[ string ] 如果string不为空(长度大于0),则判断为真
[ -n string ] 如果string不为空(长度大于0),则判断为真,和上面写法等价
[ -z string ] 如果string长度为零,则判断为真
[ str1 = str2 ] str1和str2相同,则判断为真
[ str1 == str2 ] str1和str2相同,则判断为真,等价于 [ str1 = str2 ]
[ str1 '>' str2 ] 如果按照字典顺序str1排在str2后面,则判断为真。>需要引号引起来,它们会被 shell 解释为重定向操作符
[ str1 '<' str2 ] 如果按照字典顺序str1排在str2前面,则判断为真
str=maybe
if [ -z "$str" ]; then
echo "$str is empty" >&2
elif [ "$str" = "yes" ]; then
echo "$str = yes"
elif [ "$str" = "no" ]; then
echo "$str = no"
elif [ "$str" = "maybe" ]; then
echo "$str = maybe"
fi
字符串判断时,变量要放到双引号中,例如[ -n "$var" ]
否则可能会报参数过多,或者变量为空时,命令会变成[ -n ]
,表达式为真!这是非预期的。如果放在双引号中会变成 [ -n "" ]
,表达式为假
[ -n ]; echo $? # 0
[ -n "" ]; echo $? # 1
str="hello world"; [ -n $str ]; echo $? # [: hello: binary operator expected
bash支持三种循环:for、while、until
num=0;
while [ "$num" -lt 10 ]; do
echo "num=$num"
num=$(($num+1))
done;
# 循环终止条件为 满足 num >= 10
num=0;
until [ "$num" -gt 10 ]; do
echo "num=$num"
num=$(($num+1))
done;
until循环与while循环恰好相反,只要不符合判断条件(判断条件失败),就不断循环执行指定的语句。一旦符合判断条件,就退出循环。
for in: 语法:
for variable in list
do
commands
done
例子:
for i in word1 word2 word3; do
echo $i;
done;
for i in *.sh; do
ls -l $i;
done;
for: 语法
for (( expression1; expression2; expression3 )); do
commands
done
例子:
for (( i=0; i<5; i=i+1 )); do
echo $i
done
-
函数定义:
# 第一种 fn() { # codes } # 第二种 function fn() { # codes }
-
函数调用:直接函数名,参数以空格跟在后面
function hello() { echo "Hello $1" } hello world # Hello world
-
函数参数 和脚本参数类似:
$0
: 函数所在脚本名$1
~$9
: 函数的参数,第10个使用${10}
$#
: 参数个数$@
: 所有参数以空格分隔func1() { echo "\$@: $@"; echo "\$0 \$1 \$2 \$3: $0 $1 $2 $3" echo "\$#: $#"; } func1 param1 param2 # $@: param1 param2 # $0 $1 $2 $3: ./test.sh param1 param2 # $#: 2
-
查看已声明的函数
declare -f # 查看函数名及函数体 declare -F # 只查看函数名
-
用local在函数体内声明局部变量
-
函数体内声明的变量会成为全局变量
# 函数体内声明全局变量 func1() { echo $foo1 foo1=2 echo $foo1 } func1 echo "函数外:$foo1" # # 2 # 函数外:2
-
函数体内可以修改全局变量
# 函数体内修改全局变量 foo2=1 func2() { echo $foo2 foo2=2 echo $foo2 } func2 echo "函数外:$foo2" # 1 # 2 # 函数外:2
-
函数体内使用 local 声明局部变量
# 函数体内使用 local 声明局部变量 func3() { local foo3=3 echo $foo3 } func3 echo "函数外:$foo3" # 3 # 函数外:
-
array[0]=1
array[1]=2
array[2]=3
array1=(4 5 6)
array2=(
7
8
9
)
array3=(*.sh)
# 读取
echo ${array[0]} # 1
echo $array[0] # 1[0]
echo ${array[@]} # 1 2 3
echo ${array1[@]} # 4 5 6
echo ${array2[@]} # 7 8 9
echo ${array3[@]} # source_test.sh test.sh
# 数组在赋值或者访问时如果直接使用数组名则访问的是下标0的元素
array4=(1 2 3)
array4=4
echo $array4 # 4
echo ${array4[@]} # 4 2 3
# 数组长度
echo ${#array4[@]} # 3
# 遍历数组
for i in "${array[@]}"; do
echo $i;
done;
src_arr=(1 2 3)
# 复制数组
src_copy=( "${src_arr[@]}" )
echo ${src_copy[@]}
# 给数组追加元素 的2种方式
src_arr=( "${src_arr[@]}" 4 )
echo ${src_arr[@]} # 1 2 3 4
src_arr+=(5 6)
echo ${src_arr[@]} # 1 2 3 4 5 6
Ref: