从本节开始,代码块中,紧接着的下一行的注释表示上一行命令的输出。
输入:命令
输出:#命令的结果
shell脚本
shell脚本是复杂度更高的工具,在本节中,我们使用bash
脚本,因为它最流行。
给变量赋值
foo=bar #foo是变量,bar是值
echo $foo
# bar
注意!=
两边不能有空格!因为在shell脚本中,空格的作用是分割参数。
假如你foo = bar
,解释器会将foo
视为命令,=
和bar
视为它的参数,接着报错:Command 'foo' not found
。
所以,如果文件名带有空格,我们要把它们用引号括起来。
两种定义字符串的方式
在bash中,'
和"
是不一样的。
'
是原始字符串,它会把它引起来的文字原原本本地输出,而不会去识别其中的转义字符。
既然,'
不会去转义,那么"
就是识别转义的引号了。
foo=barrr
echo $foo
#barrr
echo '$foo'
# $foo
echo "$foo"
# barrr
函数
创建一个mcd.sh的文件,vim mcd.sh
里面写上:
mcd(){
mkdir -p "$1" # $1代表第一个参数
cd "$1"
}
接着保存退出
source mcd.sh
mcd 'hello world' #这样就会创建并进入一个名为'hello world'的文件夹
#ps:如果传入多个参数,会出现一些奇奇怪怪的情况
常用变量
$0
- 脚本名$1
~$9
- 传入的参数,$1
为第一个,类推$@
- 所有参数$#
- 参数个数$?
- 前一个命令的返回值$$
- 当前脚本的PID!!
- 完完整整的上一条命令(方便sudo !!
)$_
- 上一条命令的最后一个参数。如果是交互式的shell,按Esc
后再按.
或者是按住Alt
+.
都可以获取该值。
短路运算符||
和&&
命令通常用STDOUT
返回输出值,STDERR
返回错误码和错误
true
的错误码是0
,false
的错误码是1
||
:只有左边的失败,才执行右边的命令(否则,右边的命令被短路了)。即左边为假返回右边。
false || echo "It will be printed"
# It will be printed
true || echo "It will be printed"
echo $? #上条命令的返回值
# 0
&&
:都执行,左边为真返回右边。
命令替换
$(命令)
可以代替该命令的结果
pwd
# /home/username/a/a/a
foo=$(pwd)
echo $foo
# /home/username/a/a/a
echo "We are in $(pwd)"
# We are in /home/username/a/a/a
进程替换
<(命令)
会将结果输出到一个临时文件里,并将<(命令)
替换成这个临时文件名
cat <(ls ..)
# a
# mcd.sh
# sys_backup.bak
An example
这段脚本会遍历我们提供的参数,使用grep 搜索字符串 foobar,
如果没有找到,则将其作为注释追加到文件中。
#example.sh
#!/bin/bash
echo "Starting program at $(date)" # date会被替换成日期和时间
echo "Running program $0 with $# arguments with pid $$"
for file in "$@"; do
grep foobar "$file" > /dev/null 2> /dev/null
# When pattern is not found, grep has exit status 1
# We redirect STDOUT and STDERR to a null register since we do not care about them
if [[ $? -ne 0 ]]; then
echo "File $file does not have any foobar, adding one"
echo "# foobar" >> "$file"
fi
done
通配(globbing)
通配符
?
表示一个字符,*
表示任意个字符
ls *.sh
# example.sh mcd.sh
ls ? #它也会列出所有子文件夹中符合情况的
# a b c d
花括号{}
如果许多的指令包含一段公共子串,可以用花括号自动展开这些命令
例如,convert a.{jpg,png}
就相当于convert a.jpg a.png
多个花括号可以形成笛卡尔积
touch a{1,2,3}{e,f,g}
ls
# a1e a1f a1g a2e a2f a2g a3e a3f a3g
..
表示“xx到xx”
touch {a..f}
ls
# a b c d e f
shebang
如果要运行一个.py
文件,我们可以python xxx.py
,但是如果要直接在shell里就能执行,则需要在文件开头加上shebang
:即#!
#!/usr/bin/env python
# filename: script.py
# 使用env可以自动在环境变量里面寻找python的解释器,这样兼容性更广
import sys
for arg in reversed(sys.argv[1:]):
print(arg)
shellcheck
shellcheck
可以给出warning和语法错误
shellcheck mcd.sh
In mcd.sh line 4:
cd "$1"
^-----^ SC2164: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.
Did you mean:
cd "$1" || exit
For more information:
https://www.shellcheck.net/wiki/SC2164 -- Use 'cd ... || exit' or 'cd ... |...
上面的意思是,如果cd
命令发生了错误,就应该停止脚本的运行,而不是接着在错误的路径做错误的事。
shell函数与脚本的不同点
写的要运行的bash脚本和要载入shell的东西是不一样的。
- 函数必须写与shell相同的语言,而脚本可以用任何语言编写(with shebang)
- 函数只在定义时被加载(修改定义后重新加载),而脚本每次执行时都会加载。
- 函数在当前shell环境中执行,所以函数可以对环境变量进行修改(例如修改当前工作目录)。而脚本在单独的进程中执行,所以脚本不行。脚本需要使用
export
将环境变量导出,并将值传递给环境变量。
shell工具
说明书
man
很好用,但是对于某些工具,它过于详细、多了。所以,我们可以使用社区贡献的工具tldr
来查阅常用用法。
查找文件——grep和rg
find
命令是几乎每个unix都有的工具。
# 查找所有名称为src的文件夹
find . -name src -type d
# 查找所有文件夹路径中包含test的python文件
find . -path '**/test/*.py' -type f #两个*表示未知的多个
# 查找前一天修改的所有文件
find . -mtime -1
# 查找所有大小在500k至10M的tar.gz文件
find . -size +500k -size -10M -name '*.tar.gz'
# find找到之后还可以做别的
# 删除全部扩展名为.tmp 的文件
find . -name '*.tmp' -exec rm {} \;
# 查找全部的 PNG 文件并将其转换为 JPG
find . -name '*.png' -exec convert {} {}.jpg \;
fd
也是个非常好用的工具
locate
可以查找文件系统中具有指定子串的路径,使用updatedb
可以更新它。
如果想找文件内的,可以使用grep
和rg
grep -R foobar . #在本文件夹内递归地寻找“foobar”子串,这个也可以精确到文件内
rg "import requests" -t py ~/scratch #rg - recursively search current directory for lines matching a pattern
rg "import requests" -t py -C 5 ~/scratch #如果找到了匹配的字符串,就把上下5行也一起显示出来
rg -u --files-without-match "^#\!" -t sh # -u: 不忽略隐藏文件,--files...-match: 打印所有不匹配这个模式的内容, -t sh: 只搜索.sh后缀的文件
查找历史命令——history
history
命令可以打印出命令的历史记录,加上grep
可以查找输入过的包含特定字符串的命令。而组合键Ctrl + R
大部分情况下可以按执行时间倒序搜索,在输入要找的字符串后,接着按Ctrl + R
可以一条一条地往回找。
history
# 1 getfacl -R / > ./linux.chmod.bak
# 2 sudo apt install acl
# 3 getfacl -R / > ./linux.chmod.bak
# 4 sudo getfacl -R / > ./linux.chmod.bak
# 5 su
# ......
history | grep su
# 2 sudo apt install acl
# 4 sudo getfacl -R / > ./linux.chmod.bak
# 5 su
# 6 sudo passwd root
# 7 su
# ......
(Ctrl + R)
#(reverse-i-search)`':
#(reverse-i-search)`rg': history | grep rg #输入"rg"
#(reverse-i-search)`rg': man rg #再按一次Ctrl + R
高级货——fzf
fzf
也是个查找的命令。它可以模糊搜索,实时地显示结果。而grep
想要做到这一点,就需要使用正则表达式
cat example.sh | fzf #可以搜索example.sh里面的内容
好用的shell
zsh
和fish
都有“输入命令,动态查找历史命令”的功能。按一下→
就可以选中它了。
友好地显示文件夹内容——tree
tree
命令可以以树状结构打印文件夹里面的内容
比tree更“人性化”的broot
broot
像tree
一样,可以以树状结构打印,但是它多了许多功能。它不会全部显示出来,而是让你可以翻页。它还能像fzf
一样模糊匹配这些文件
另外,nnn
也不错,可以利用箭头来浏览文件夹。