从本节开始,代码块中,紧接着的下一行的注释表示上一行命令的输出。

输入:命令
输出:#命令的结果

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的错误码是0false的错误码是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可以更新它。

如果想找文件内的,可以使用greprg

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想要做到这一点,就需要使用正则表达式

image.png

cat example.sh | fzf    #可以搜索example.sh里面的内容

好用的shell

zshfish都有“输入命令,动态查找历史命令”的功能。按一下就可以选中它了。
image.png

友好地显示文件夹内容——tree

tree命令可以以树状结构打印文件夹里面的内容
image.png

比tree更“人性化”的broot

broottree一样,可以以树状结构打印,但是它多了许多功能。它不会全部显示出来,而是让你可以翻页。它还能像fzf一样模糊匹配这些文件
image.png

另外,nnn也不错,可以利用箭头来浏览文件夹。

SUFE大二在读