# Shell笔记
# 初识shell
相信很多人第一次使用的命令肯定是ping或者ipconfig这类
shell、bash、zsh、terminal、终端、命令行界面、CLI(Command Line Interface)、iTerm、cmd、powershell,很长一段时间,这些名词都让我感到很困惑,如果你也跟我一样,现在可以跟着我一起探索他们之间的区别。
早期的计算机或现在的Linux服务器并没有图形界面,你面对的是一片黑色的屏幕,想让电脑执行你的操作命令,敲下命令字符按下回车键就行,比如我打开电脑桌面,删除我已经发了朋友圈的美照,可以分别敲如下两行命令:
cd ~/Desktop
rm handsomeboy.jpg
那么cd
、rm
到底是什么?它们藏在电脑的哪里?又是谁把我的命令交给它们执行的?
在macOS执行which
命令可以知道命令在哪里:
➜ which cd
cd: shell built-in command
➜ which rm
/bin/rm
可以看到cd命令是内置的命令,rm是在/bin目录下的一个可执行文件。
那么是谁把我的命令rm handsomeboy.jpg
交给电脑让电脑删除的?
这个家伙就是shell,shell是实际处理用户命令字符并返回输出结果的程序。
阮一峰的Bash教程 (opens new window)里说到:
Shell 这个单词的原意是“外壳”,跟 kernel(内核)相对应,比喻内核外面的一层,即用户跟内核交互的对话界面。
输入下面的命令可以查看你的电脑里有多少shell种类:
cat /etc/shells
macOS默认的shell最开始是bash,在macOS Catalina(即10.15)之后的系统版本,它变成了zsh (opens new window)。
终端
终端指的是运行shell程序的程序,听起来有点绕,你可以理解成终端是shell运行所需的环境,就像鱼儿游泳需要水这个环境一样。
终端长什么样?它通常是一个用户输入命令行的窗口,比如macOS的终端、iTerm,或者Windows系统的CMD命令提示符、PowerShell...
好了,到这里你应该对Shell脚本有了个大概的了解,接下来让我们来看看一下Shell脚本的初始化配置和一些常用命令。
# Shell和终端配置
知道shell的一些概念后,我们来配置一下自己电脑的终端,让我们更方便地使用
# 切换Shell
#查看当前SHELL
echo $SHELL
#切换到古老的bash
chsh -s /bin/bash
#或者切换到你喜欢的zsh
chsh -s /bin/zsh
# Linux系统可以这样 https://askubuntu.com/a/131838
chsh -s $(which zsh)
# Shell配置文件加载顺序:
macOS的zsh:.zshrc
-> .zlogin
macOS切换到/bin/bash:.bash_profile -> .profile
测试过程:
cd到个人目录(cd ~),在.bash_profile、.bashrc、的第一行分别输出echo ".mkshrc"echo ".profile"echo ".zlogin"echo ".zshrc",新开一个终端窗口就能知道了
安装命令行程序
flutter upgrade
macOS 上的终极 Shell - 池建强 (opens new window)
# 我的常用代码片段
# 文件操作
# 文件和文件夹操作
# 在桌面创建文件夹,名字叫test
mkdir -p ~/Desktop/test
# 文件夹test改名叫test2
mv ~/Desktop/test ~/Desktop/test2
# 创建文件test.txt
vim ~/Desktop/test/test.txt
# 删除文件夹,r是递归删除子目录的所有文件,f是强制删除(可选)
rm -rf ~/Desktop/test
# 删除文件
rm ~/Desktop/test.txt
# 列出文件夹下的文件
ls ~/Desktop/test
# 列出文件夹下的文件,包括子文件夹
ls -R ~/Desktop/test
# 列出文件夹下的文件,包括子文件夹,并且显示文件的大小
ls -Rl ~/Desktop/test
# 创建文本文件
touch ~/Desktop/test.txt
# 编辑文本文件
vim ~/Desktop/test.txt
#
# 获取当前脚本文件所在的绝对路径
current_path="$(pwd)"
# 或者
current_path=`pwd`
# 或者
current_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# 输出current_path的值
echo $current_path
#打开cd到父目录的终端窗口
echo path="$(dirname "$0")"
echo $path
cd "$(dirname "$0")"
# 在终端重新打开一个窗口(当前文件夹)
open -a Terminal "$(dirname "$0")"
# 获取当前脚本文件的名字:
shell_file_name=`basename "$0"`
echo "[Debug] $shell_file_name"
# 文本操作
# sed
# ----------sed----------
# 选项-i是直接编辑文件
# 删readme.md中以panda开头的行(包含panda的直接sed -i "" "/panda/d" readme.md)
sed -i "" "/^panda/d" readme.md
# 删除文件每行出现的FFF1字符,不是这一整行
sed -i "" "/FFF1/d" yjyjyj.log
# 删除包含"发送蓝牙数据"的行(一整行)
sed -i '' '/发送蓝牙数据/d' ./yjyjyj.log
# 给每一行加前缀 >> https://stackoverflow.com/a/2099478
sed -i -e 's/^/prefix/'filepath
# 给特定的行加前缀(macOS,linux不需要-i '') https://unix.stackexchange.com/a/336774
sed -i '' 's/^Hellow2/Z&/' test.txt
其他未测试sed
https://wangchujiang.com/linux-command/c/sed.html
全面替换标记g
使用后缀 /g 标记会替换每一行中的所有匹配:
sed 's/book/books/g' file
当需要从第N处匹配开始替换时,可以使用 /Ng:
echo sksksksksksk | sed 's/sk/SK/2g'
skSKSKSKSKSK
echo sksksksksksk | sed 's/sk/SK/3g'
skskSKSKSKSK
echo sksksksksksk | sed 's/sk/SK/4g'
skskskSKSKSK
定界符
以上命令中字符 / 在sed中作为定界符使用,也可以使用任意的定界符:
sed 's:test:TEXT:g'
sed 's|test|TEXT|g'
定界符出现在样式内部时,需要进行转义:
sed 's/\/bin/\/usr\/local\/bin/g'
感谢 https://wangchujiang.com/linux-command/c/sed.html https://unix.stackexchange.com/a/78626
# sed -i "" "s/<key>CFBundleInfoDictionaryVersion<\/key>\s{1,8}<string>6.0<\/string>/<key>CFBundleInfoDictionaryVersion2333<\/key>/g" Info.plist
# sed -i -E "" "s/(<key>.+)BundleInfoDictionary(.+<\/key>)/\1something\2/" Info.plist
# sed -i "s/<username><![CDATA[name]]><\/username>/something/g" file.xml
# sed -i -E "s/(<username>.+)name(.+<\/username>)/\1something\2/" file.xml
# 下一个:n命令
# 如果test被匹配,则移动到匹配行的下一行,替换这一行的aa,变为bb,并打印该行,然后继续:
# sed '/test/{ n; s/aa/bb/; }' file
# 测试通过
sed -i "" '/CFBundleInfoDictionaryVersion/{ n; s/<string>.*<\/string>/<string>6666<\/string>/; }' Info.plist
# 可以
# sed -i "" '/CFBundleInfoDictionaryVersion/{ n; s/6/6666/; }' Info.plist
# base64编解码
echo "I love you" | base64
#output: SSBsb3ZlIHlvdQo=
echo "SSBsb3ZlIHlvdQo=" | base64 -D
#等价于:
echo SSBsb3ZlIHlvdQo= | base64 -D
#等价于:
echo SSBsb3ZlIHlvdQo= | base64 --decode
#output: I love you
# tail
# 打印文本access.log的最后20行(> panda.txt表示把这20行生成新文件panda.txt)
tail -n 20 access.log > panda.txt
# macOS测试可用
tail -100 -F ~/.V2rayU/v2ray-core.log
# macOS专属命令
MacOS配置HTTP和HTTPS代理 (opens new window)
$ networksetup -setwebproxy "Wi-Fi" 127.0.0.1 1087
$ networksetup -setsecurewebproxy "Wi-Fi" 127.0.0.1 1087
让终端说话
say -v Ting-Ting 你好,我就是婷婷
弹出通知消息
osascript -e 'display notification "继续你的流程吧" with title "iOS打包完成"'
# 字符串相关
拼接字符串
your_name="runoob"
# 使用双引号拼接
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting $greeting_1 #hello, runoob ! hello, runoob !
# 使用单引号拼接
greeting_2='hello, '$your_name' !'
greeting_3='hello, ${your_name} !'#不推荐!
echo $greeting_2 $greeting_3 #hello, runoob ! hello, ${your_name} !
获取字符串长度
string="abcd"
echo ${#string} #输出 4
# 字符串包含
# https://stackoverflow.com/a/229606/4493393
string='My long string'
if [[ $string == *"My long"* ]]; then
echo "It's there!"
fi
string='My string';
if [[ $string =~ "My" ]]; then
echo "It's there!"
fi
case "$string" in
*foo*)
# Do stuff
;;
esac
提取子字符串 以下实例从字符串第 2 个字符开始截取 4 个字符:
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo
查找子字符串 查找字符 i 或 o 的位置(哪个字母先出现就计算哪个):
string="runoob is a great site"
echo `expr index "$string" io` # 输出 4
# 数组
# 定义数组
array_name=(value0 value1 value2 value3)
或者
array_name=(
value0
value1
value2
value3
)
单独定义数组的各个分量:
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen
# 将命令的输出变成数组
launchimages=( $(find ./app -name 'welcome.png') )
declare -p launchimages
# 读取及遍历数组
读取数组元素值的一般格式是:
${数组名[下标]}
例如:
valuen=${array_name[n]}
使用 @ 符号可以获取数组中的所有元素,例如:
echo ${array_name[@]}
遍历
https://stackoverflow.com/a/8880633/4493393
## declare an array variable
declare -a arr=("element1" "element2" "element3")
## now loop through the above array
for i in "${arr[@]}"
do
echo "$i"
# or do whatever with individual element of the array
done
# You can access them using echo "${arr[0]}", "${arr[1]}" also
# 获取数组的长度
获取数组长度的方法与获取字符串长度的方法相同,例如:
# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}
# read - 读取用户终端输入
funWithReturn(){
echo "这个函数会对输入的两个数字进行相加运算..."
echo "输入第一个数字: "
read aNum
echo "输入第二个数字: "
read anotherNum
echo "两个数字分别为 $aNum 和 $anotherNum !"
return $(($aNum+$anotherNum))
}
# 其他
一次性创建Git仓库并提交
git init &&git add . &&git commit -m '1st'
将某目录下全部文件的所有者改为本用户
eg:MacOS A、B、C...多账户登录时,/usr/local/lib/node_modules/ 此目录读写权限只能是用户A、B、C等其中的一个
sudo chown -R $(whoami) /usr/local/lib/node_modules/
将当前目录下所有文件读写权限改为可读可写
sudo chmod -R 777 ./
#加入环境变量
vim ~/.bash_profile
#加入path
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:${JAVA_HOME}:${JAVA_HOME}/bin"
#重启profile
source ~/.bash_profile
# 别名alias
alias cls='clear'
alias ll='ls -l'
alias la='ls -a'
alias vi='vim'
alias javac="javac -J-Dfile.encoding=utf8"
alias grep="grep --color=auto"
alias -s html=mate # 在命令行直接输入后缀为 html 的文件名,会在 TextMate 中打开
alias -s rb=mate # 在命令行直接输入 ruby 文件,会在 TextMate 中打开
alias -s py=vi # 在命令行直接输入 python 文件,会用 vim 中打开,以下类似
alias -s js=vi
alias -s c=vi
alias -s java=vi
alias -s txt=vi
alias -s gz='tar -xzvf'
alias -s tgz='tar -xzvf'
alias -s zip='unzip'
alias -s bz2='tar -xjvf'
#我的
alias zshconfig="open -a 'Sublime Text' ~/.zshrc"
# 命令组合符&& 与 || 、分号分隔符 ;
分号用于多条命令间的分隔符。多条命令中间的特殊符号还有 “&&”和”||”:
command1 ; command2
command1 && command2
command1 || command2
使用”;”时,不管command1是否执行成功都会执行command2;
使用”&&”时,只有command1执行成功后,command2才会执行,否则command2不执行;
使用”||”时,command1执行成功后command2 不执行,否则去执行command2.
# exit 0、exit 1、exit 2
exit 0:成功
exit 1 :一般错误,杂项错误,如“除以零”和其他不允许的操作
exit 2:内置程序的误用
# 查看文件权限
可以直接通过 man chmod
在终端工具上查看chmod
命令的帮助手册。
ls -l
命令可以查看当前目录下所有文件的访问权限,也可以查看指定文件。比如,查看 Tomcat bin 目录中的 startup.sh 文件的访问权限时:
yifeng:bin yifeng$ ls -l startup.sh
-rwxrwxrwx@ 1 yifeng staff 1904 9 27 18:32 startup.sh
上面打印的文件信息中每一部分所代表的含义,分别对应如下解释:
文件类型和访问权限 文件数量 所属用户 所在群组 文件大小 修改日期(月 日 时 分) 文件名称
第一部分详细说明一下,就以 “-rwxrwxrwx” 为例:第一个符号代表文件类型, “-” 符号表示该文件是非目录类型,“d” 符号表示目录类型;( 末尾的 @ 符号表示文件拓展属性,属于文件系统的一个功能。)
后面九个字母分为三组,从前到后每组分别对应所属用户(user)、所属用户所在组(group)和其他用户(other)对该文件的访问权限;
每组中的三个字符 “rwx” 分别表示对应用户对该文件拥有的可读/可写/可执行权限,没有相应权限则使用 “-” 符号替代。
#修改访问权限
根据上面查看权限部分的介绍,修改权限也应包括访问用户、添加或取消操作、具体权限和访问文件,即:
chmod 用户+操作+权限 文件
用户部分:使用字母 u 表示文件拥有者(user),g 表示拥有者所在群组(group),o 表示其他用户(other),a 表示全部用户(all,包含前面三种用户范围);
操作部分:“+” 符号表示增加权限,“-” 符号表示取消权限,“=” 符号表示赋值权限;
权限部分:“r” 符号表示可读(read),“w” 表示可写(write),“x” 表示可执行权限(execute);
文件部分:如不指定文件名,表示操作对象为当前目录下的所有文件。
还以前面 startup.sh 文件为例,将拥有者所在群组和其他用户改为可读可写权限、取消可执行权限的使用方式为:
chmod go-x startup.sh
然后使用 ls 命令查看权限,
yifeng:bin yifeng$ ls -l startup.sh
-rwxrw-rw-@ 1 yifeng staff 1904 9 27 18:32 startup.sh
可以看到,文件访问权限已经按照要求发生对应变化。
如果是复杂一点操作的话,可以同时使用多种操作符添加和取消权限,并且可以使用 “,” 符号同时对不同用户范围修改权限,比如:
chmod g+x,o+x-w startup.sh 还有一种简单的写法,使用数字表示权限部分的读/写/可执行权限类型。数字和权限类型的对应关系,可以从这张图中直观地看出来:
即,1 表示可执行,2 表示可写,4 表示可读。每种类型数字相加所得到的值表示交叉部分的公共类型。
这样的话,使用三个数字便可以分别代表三种不同用户类型的权限修改结果。比如,修改所有用户的访问权限均为可读可写可执行(rwx)的话,这样使用即可:
chmod 777 startup.sh
三个数字从前到后分别表示 u、g、o 三种用户类型的访问权限,使用时按需修改。
补充一点,有时候需要递归修改目录文件及其子目录中的文件类型,可以使用 -R 选项
# 带颜色输出
declare -A colors
#curl www.bunlongheng.com/code/colors.png
# Reset
colors[Color_Off]='\033[0m' # Text Reset
# Regular Colors
colors[Black]='\033[0;30m' # Black
colors[Red]='\033[0;31m' # Red
colors[Green]='\033[0;32m' # Green
colors[Yellow]='\033[0;33m' # Yellow
colors[Blue]='\033[0;34m' # Blue
colors[Purple]='\033[0;35m' # Purple
colors[Cyan]='\033[0;36m' # Cyan
colors[White]='\033[0;37m' # White
# Bold
colors[BBlack]='\033[1;30m' # Black
colors[BRed]='\033[1;31m' # Red
colors[BGreen]='\033[1;32m' # Green
colors[BYellow]='\033[1;33m' # Yellow
colors[BBlue]='\033[1;34m' # Blue
colors[BPurple]='\033[1;35m' # Purple
colors[BCyan]='\033[1;36m' # Cyan
colors[BWhite]='\033[1;37m' # White
# Underline
colors[UBlack]='\033[4;30m' # Black
colors[URed]='\033[4;31m' # Red
colors[UGreen]='\033[4;32m' # Green
colors[UYellow]='\033[4;33m' # Yellow
colors[UBlue]='\033[4;34m' # Blue
colors[UPurple]='\033[4;35m' # Purple
colors[UCyan]='\033[4;36m' # Cyan
colors[UWhite]='\033[4;37m' # White
# Background
colors[On_Black]='\033[40m' # Black
colors[On_Red]='\033[41m' # Red
colors[On_Green]='\033[42m' # Green
colors[On_Yellow]='\033[43m' # Yellow
colors[On_Blue]='\033[44m' # Blue
colors[On_Purple]='\033[45m' # Purple
colors[On_Cyan]='\033[46m' # Cyan
colors[On_White]='\033[47m' # White
# High Intensity
colors[IBlack]='\033[0;90m' # Black
colors[IRed]='\033[0;91m' # Red
colors[IGreen]='\033[0;92m' # Green
colors[IYellow]='\033[0;93m' # Yellow
colors[IBlue]='\033[0;94m' # Blue
colors[IPurple]='\033[0;95m' # Purple
colors[ICyan]='\033[0;96m' # Cyan
colors[IWhite]='\033[0;97m' # White
# Bold High Intensity
colors[BIBlack]='\033[1;90m' # Black
colors[BIRed]='\033[1;91m' # Red
colors[BIGreen]='\033[1;92m' # Green
colors[BIYellow]='\033[1;93m' # Yellow
colors[BIBlue]='\033[1;94m' # Blue
colors[BIPurple]='\033[1;95m' # Purple
colors[BICyan]='\033[1;96m' # Cyan
colors[BIWhite]='\033[1;97m' # White
# High Intensity backgrounds
colors[On_IBlack]='\033[0;100m' # Black
colors[On_IRed]='\033[0;101m' # Red
colors[On_IGreen]='\033[0;102m' # Green
colors[On_IYellow]='\033[0;103m' # Yellow
colors[On_IBlue]='\033[0;104m' # Blue
colors[On_IPurple]='\033[0;105m' # Purple
colors[On_ICyan]='\033[0;106m' # Cyan
colors[On_IWhite]='\033[0;107m' # White
color=${colors[$input_color]}
white=${colors[White]}
# echo $white
for i in "${!colors[@]}"
do
echo -e "$i = ${colors[$i]}I love you$white"
done
echo -e "\033[背景;字体颜色m 字符串\033[0m"
eg : echo -e "\033[30m 黑色字 \033[0m"
echo -e "\033[1;31m This is red text \033[0m"
#我的例子,避免
alias nvminit="[[ -s $HOME/.nvm/nvm.sh ]] && . $HOME/.nvm/nvm.sh"
alias npm='echo -e "如果提示command not found,请先执行:\033[1;31m nvminit \033[0m" && npm'
# Tips
# 有个脚本:project/docs/xcode_config.sh,里面有个函数 getip
# 在project目录下调用getip的两种方式:
# 方法一
source ./docs/xcode_config.sh
getip
# 方法二 https://stackoverflow.com/a/42101141/4493393
source $(dirname "$0")/docs/xcode_config.sh
getip
# 详细教程
文章部分参考自阮一峰的《Bash 脚本教程》:https://wangdoc.com/bash/intro.html
Shell 这个单词的原意是“外壳”,跟 kernel(内核)相对应,比喻内核外面的一层,即用户跟内核交互的对话界面。
首先,Shell 是一个程序,提供一个与用户对话的环境。这个环境只有一个命令提示符,让用户从键盘输入命令,所以又称为命令行环境(commandline,简写为 CLI)。Shell 接收到用户输入的命令,将命令送入操作系统执行,并将结果返回给用户。本书中,除非特别指明,Shell 指的就是命令行环境。
其次,Shell 是一个命令解释器,解释用户输入的命令。它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 命令写出各种小程序,又称为脚本(script)。这些脚本都通过 Shell 的解释执行,而不通过编译。
最后,Shell 是一个工具箱,提供了各种小工具,供用户方便地使用操作系统的功能。
Shell 有很多种,只要能给用户提供命令行环境的程序,都可以看作是 Shell。
历史上,主要的 Shell 有下面这些。
- Bourne Shell(sh)
- Bourne Again shell(bash)
- C Shell(csh)
- TENEX C Shell(tcsh)
- Korn shell(ksh)
- Z Shell(zsh)
- Friendly Interactive Shell(fish)
Bash 是目前最常用的 Shell,也曾经是MacOS默认的Shell(最新的MacOS 10.15默认使用zsh),除非特别指明,下文的 Shell 和 Bash 当作同义词使用,可以互换。
下面的命令可以查看当前运行的 Shell。
$ echo $SHELL
/bin/bash
下面的命令可以查看当前的 Linux 系统安装的所有 Shell。
$ cat /etc/shells
上面两个命令中,$
是命令行环境的提示符,用户只需要输入提示符后面的内容。
Linux 允许每个用户使用不同的 Shell,用户的默认 Shell 一般都是 Bash,或者与 Bash 兼容。
如果是不带有用户界面的、没有任何按钮和图标的 Linux 系统(比如专用于服务器的系统),启动后就直接是命令行环境,你需要输入命令来查看、编辑文件等。
不过,现在大部分的 Linux 发行版,尤其是针对普通用户的发行版,都是图形界面环境,比如Ubuntu系统。用户登录系统后,自动进入像Windows和MacOS一样的图形界面,需要自己启动终端模拟器,才能进入命令行环境。
终端模拟器(terminal emulator)就是一个模拟命令行窗口的程序,让用户在一个窗口中使用命令行环境,并且提供各种附加功能,比如调整颜色、字体大小、行距等等。
不同 Linux 发行版(准确地说是不同的桌面环境)带有的终端程序是不一样的,比如 KDE 桌面环境的终端程序是 konsole,Gnome 桌面环境的终端程序是 gnome-terminal,用户也可以安装第三方的终端程序。所有终端程序,尽管名字不同,基本功能都是一样的,就是让用户可以进入命令行环境,使用 Shell。
# 命令行提示符
进入命令行环境以后,用户会看到 Shell 的提示符。提示符往往是一串前缀,最后以一个美元符号$
结尾,用户可以在这个符号后面输入各种命令。
[user@hostname] $
上面例子中,完整的提示符是[user@hostname] $
,其中前缀是用户名(user
)加上@
,再加主机名(hostname
)。比如,用户名是bill
,主机名是home-machine
,前缀就是bill@home-machine
。
注意,根用户(root)的提示符,不以美元符号($
)结尾,而以井号(#
)结尾,用来提醒用户,现在具有根权限,可以执行各种操作,务必小心,不要出现误操作。这个符号是可以自己定义的,详见《命令提示符》一章。
为了简洁,后文的命令行提示符都只使用$
表示。
# 进入和退出方法
进入命令行环境以后,一般就已经打开 Bash 了。如果你的 Shell 不是 Bash,可以输入bash
命令启动 Bash。
$ bash
退出 Bash 环境,可以使用exit
命令,也可以同时按下Ctrl + d
。
$ exit
Bash 的基本用法就是在命令行输入各种命令,非常直观。作为练习,可以试着输入pwd
命令。按下回车键,就会显示当前所在的目录。
$ pwd
/home/me
如果不小心输入了pwe
,会返回一个提示,表示输入出错,没有对应的可执行程序。
$ pwe
bash: pwe:未找到命令
Shell 和 Bash 的历史 (opens new window)
# Bash 的基本语法
# echo 命令(难度1 使用率9)
echo
命令的作用是输出一行文本,可以将该命令的参数原样输出。
$ echo hello world
hello world
上面例子中,echo
的参数是hello world
,可以原样输出。
如果想要输出的是多行文本,即包括换行符。这时需要把多行文本放在引号里面。
$ echo "<HTML>
<HEAD>
<TITLE>Page Title</TITLE>
</HEAD>
<BODY>
Page body.
</BODY>
</HTML>"
上面例子中,echo
可以原样输出多行文本。
# -n
参数(难度2 使用率3)
默认情况下,echo
输出的文本末尾会有一个回车符。-n
参数可以取消末尾的回车符,使得下一个提示符紧跟在输出内容的后面。
$ echo -n hello world
hello world$
上面例子中,world
后面直接就是下一行的提示符$
。
$ echo a;echo b
a
b
$ echo -n a;echo b
ab
上面例子中,-n
参数可以让两个echo
命令的输出连在一起,出现在同一行。
# -e
参数(难度2 使用率4)
-e
参数会解释引号(双引号和单引号)里面的特殊字符(比如换行符\n
)。如果不使用-e
参数,即默认情况下,引号会让特殊字符变成普通字符,echo
不解释它们,原样输出。
$ echo "Hello\nWorld"
Hello\nWorld
# 双引号的情况
$ echo -e "Hello\nWorld"
Hello
World
# 单引号的情况
$ echo -e 'Hello\nWorld'
Hello
World
上面代码中,-e
参数使得\n
解释为换行符,导致输出内容里面出现换行。
# 命令格式
Shell 命令基本都是下面的格式:
$ command [ arg1 ... [ argN ]]
上面代码中,command
是具体的命令或者一个可执行文件,arg1 ... argN
是传递给命令的参数,它们是可选的。
$ ls -l
上面这个命令中,ls
是命令,-l
是参数。
有些参数是命令的配置项,这些配置项一般都以一个连词线开头,比如上面的-l
。同一个配置项往往有长和短两种形式,比如-l
是短形式,--list
是长形式,它们的作用完全相同。短形式便于手动输入,长形式一般用在脚本之中,可读性更好,利于解释自身的含义。
# 短形式
$ ls -r
# 长形式
$ ls --reverse
上面命令中,-r
是短形式,--reverse
是长形式,作用完全一样。前者便于输入,后者便于理解。
Bash 单个命令一般都是一行,用户按下回车键,就开始执行。有些命令比较长,写成多行会有利于阅读和编辑,这时可以在每一行的结尾加上反斜杠,Bash 就会将下一行跟当前行放在一起解释。
$ echo foo bar
# 等同于
$ echo foo \
bar
# 空格
Bash 使用空格(或 Tab 键)区分不同的参数。
$ command foo bar
上面命令中,foo
和bar
之间有一个空格,所以 Bash 认为它们是两个参数。
如果参数之间有多个空格,Bash 会自动忽略多余的空格。
$ echo this is a test
this is a test
上面命令中,a
和test
之间有多个空格,Bash 会忽略多余的空格。
# 分号
分号(;
)是命令的结束符,使得一行可以放置多个命令,上一个命令执行结束后,再执行第二个命令。
$ clear; ls
上面例子中,Bash 先执行clear
命令,执行完成后,再执行ls
命令。
注意,使用分号时,第二个命令总是接着第一个命令执行,不管第一个命令执行成功或失败。
PS:如果想第一个命令执行成功后才执行第二个命令,则要用&&
# 命令的组合符&&
和||
除了分号,Bash 还提供两个命令组合符&&
和||
,允许更好地控制多个命令之间的继发关系。
Command1 && Command2
上面命令的意思是,如果Command1
命令运行成功,则继续运行Command2
命令。
Command1 || Command2
上面命令的意思是,如果Command1
命令运行失败,则继续运行Command2
命令。
下面是一些例子。
$ cat filelist.txt ; ls -l filelist.txt
上面例子中,只要cat
命令执行结束,不管成功或失败,都会继续执行ls
命令。
$ cat filelist.txt && ls -l filelist.txt
上面例子中,只有cat
命令执行成功,才会继续执行ls
命令。如果cat
执行失败(比如不存在文件flielist.txt
),那么ls
命令就不会执行。
$ mkdir foo || mkdir bar
上面例子中,只有mkdir foo
命令执行失败(比如foo
目录已经存在),才会继续执行mkdir bar
命令。如果mkdir foo
命令执行成功,就不会创建bar
目录了。
# type 命令
Bash 本身内置了很多命令,同时也可以执行外部程序。怎么查看一个命令是内置命令还是外部程序呢?
type
命令用来判断命令的来源。
$ type echo
echo is a shell builtin
$ type ls
ls is hashed (/bin/ls)
上面代码中,type
命令告诉我们,echo
是内部命令,ls
是外部程序(/bin/ls
)。
type
命令本身也是内置命令。
$ type type
type is a shell builtin
如果要查看一个命令的所有定义,可以使用type
命令的-a
参数。
$ type -a echo
echo is shell builtin
echo is /usr/bin/echo
echo is /bin/echo
上面代码表示,echo
命令即是内置命令,也有对应的外部程序。
type
命令的-t
参数,可以返回一个命令的类型:别名(alias),关键词(keyword),函数(function),内置命令(builtin)和文件(file)。
$ type -t bash
file
$ type -t if
keyword
上面例子中,bash
是文件,if
是关键词。
# 快捷键
Bash 提供很多快捷键,可以大大方便操作。下面是一些最常用的快捷键,完整的介绍参见《行操作》一章。
Ctrl + L
:清除屏幕并将当前行移到页面顶部。Ctrl + C
:中止当前正在执行的命令。Shift + PageUp
:向上滚动。Shift + PageDown
:向下滚动。Ctrl + U
:从光标位置删除到行首。Ctrl + K
:从光标位置删除到行尾。Ctrl + D
:关闭 Shell 会话。↑
,↓
:浏览已执行命令的历史记录。
除了上面的快捷键,Bash 还具有自动补全功能。命令输入到一半的时候,可以按下 Tab 键,Bash 会自动完成剩下的部分。比如,输入pw
,然后按一下 Tab 键,Bash 会自动补上d
。
除了命令的自动补全,Bash 还支持路径的自动补全。有时,需要输入很长的路径,这时只需要输入前面的部分,然后按下 Tab 键,就会自动补全后面的部分。如果有多个可能的选择,按两次 Tab 键,Bash 会显示所有选项,让你选择。
# Bash 函数
https://wangdoc.com/bash/function.html
# 函数的声明、调用、删除
函数(function)是可以重复使用的代码片段,有利于代码的复用。它与别名(alias)的区别是,别名只适合封装简单的单个命令,函数则可以封装复杂的多行命令。
函数总是在当前 Shell 执行,这是跟脚本的一个重大区别,Bash 会新建一个子 Shell 执行脚本。如果函数与脚本同名,函数会优先执行。但是,函数的优先级不如别名,即如果函数与别名同名,那么别名优先执行。
Bash 函数定义的语法有两种。
# 第一种
fn() {
# codes
}
# 第二种
function fn() {
# codes
}
上面代码中,fn
是自定义的函数名,函数代码就写在大括号之中。这两种写法是等价的。
下面是一个简单函数的例子。
# 输出自我介绍模板
introduce() {
echo "Hello my name is $1"
}
上面代码中,函数体里面的$1
表示函数调用时的第一个参数。
调用时,就直接写函数名,参数跟在函数名后面。
$ introduce Panda
Hello my name is Panda
下面是一个多行函数的例子,显示当前日期时间。
today() {
echo -n "Today's date is: "
date +"%A, %B %-d, %Y"
}
#PPNote: 使用:echo `today`可以输出函数返回值,暂时不知道原理
删除一个函数,可以使用unset
命令。
unset -f functionName
# Tips
查看当前 Shell 已经定义的所有函数,可以使用declare
命令。
$ declare -f
上面的declare
命令不仅会输出函数名,还会输出所有定义。输出顺序是按照函数名的字母表顺序。由于会输出很多内容,最好通过管道命令配合more
或less
使用。
declare
命令还支持查看单个函数的定义。
$ declare -f functionName
declare -F
可以输出所有已经定义的函数名,不含函数体。
$ declare -F
# 参数变量
函数体内可以使用参数变量,获取函数参数。函数的参数变量,与脚本参数变量是一致的。
$1
~$9
:函数的第一个到第9个的参数。$0
:函数所在的脚本名。$#
:函数的参数总数。$@
:函数的全部参数,参数之间使用空格分隔。$*
:函数的全部参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
如果函数的参数多于9个,那么第10个参数可以用${10}
的形式引用,以此类推。
下面是一个示例脚本test.sh
。
#!/bin/bash
# test.sh
function alice {
echo "alice: $@"
echo "$0: $1 $2 $3 $4"
echo "$# arguments"
}
alice in wonderland
运行该脚本,结果如下。
$ bash test.sh
alice: in wonderland
test.sh: in wonderland
2 arguments
上面例子中,由于函数alice
只有第一个和第二个参数,所以第三个和第四个参数为空。
下面是一个日志函数的例子。
function log_msg {
echo "[`date '+ %F %T'` ]: $@"
}
使用方法如下。
$ log_msg "This is sample log message"
[ 2018-08-16 19:56:34 ]: This is sample log message
# return 命令
return
命令用于从函数返回一个值。函数执行到这条命令,就不再往下执行了,直接返回了。
function func_return_value {
return 10
}
函数将返回值返回给调用者。如果命令行直接执行函数,下一个命令可以用$?
拿到返回值。
$ func_return_value
$ echo "Value returned by function is: $?"
Value returned by function is: 10
return
后面不跟参数,只用于返回也是可以的。
function name {
commands
return
}
# 全局变量和局部变量,local 命令
Bash 函数体内直接声明的变量,属于全局变量,整个脚本都可以读取。这一点需要特别小心。
# 脚本 test.sh
fn () {
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
上面脚本的运行结果如下。
$ bash test.sh
fn: foo = 1
global: foo = 1
上面例子中,变量$foo
是在函数fn
内部声明的,函数体外也可以读取。
函数体内不仅可以声明全局变量,还可以修改全局变量。
#! /bin/bash
foo=1
fn () {
foo=2
}
fn
echo $foo
上面代码执行后,输出的变量$foo
值为2。
函数里面可以用local
命令声明局部变量。
#! /bin/bash
# 脚本 test.sh
fn () {
local foo
foo=1
echo "fn: foo = $foo"
}
fn
echo "global: foo = $foo"
上面脚本的运行结果如下。
$ bash test.sh
fn: foo = 1
global: foo =
上面例子中,local
命令声明的$foo
变量,只在函数体内有效,函数体外没有定义。