# Shell笔记

# 初识shell

相信很多人第一次使用的命令肯定是ping或者ipconfig这类

shell、bash、zsh、terminal、终端、命令行界面、CLI(Command Line Interface)、iTerm、cmd、powershell,很长一段时间,这些名词都让我感到很困惑,如果你也跟我一样,现在可以跟着我一起探索他们之间的区别。

早期的计算机或现在的Linux服务器并没有图形界面,你面对的是一片黑色的屏幕,想让电脑执行你的操作命令,敲下命令字符按下回车键就行,比如我打开电脑桌面,删除我已经发了朋友圈的美照,可以分别敲如下两行命令:

cd ~/Desktop
rm handsomeboy.jpg

那么cdrm到底是什么?它们藏在电脑的哪里?又是谁把我的命令交给它们执行的?

在macOS执行which命令可以知道命令在哪里:

which cd
cd: shell built-in commandwhich 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)

终端

img

终端指的是运行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"

# 命令组合符&& 与 || 、分号分隔符 ;

分号用于多条命令间的分隔符。多条命令中间的特殊符号还有 “&&”和”||”:

  1. command1 ; command2

  2. command1 && command2

  3. 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

上面命令中,foobar之间有一个空格,所以 Bash 认为它们是两个参数。

如果参数之间有多个空格,Bash 会自动忽略多余的空格。

$ echo this is a     test
this is a test

上面命令中,atest之间有多个空格,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命令不仅会输出函数名,还会输出所有定义。输出顺序是按照函数名的字母表顺序。由于会输出很多内容,最好通过管道命令配合moreless使用。

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变量,只在函数体内有效,函数体外没有定义。

上次更新: 3/16/2022, 12:12:12 PM