Missing Semester Notes - Shell Tools and Scripting

2020-05-15

基础的Bash用法

Shell脚本

本章介绍了基本的Bash用法

变量定义

Bash支持自定义变量,语法为foo=bar,访问变量的语法为$foo

这里需要注意foo = bar是不行的,会被解析成执行foo并传入= bar两个参数

字符串

Bash中的字符串可以用'"分隔符定义,但是这两者是有区别的。用"定义的字符串会进行变量值替换

foo=bar
echo "$foo"
# prints bar
echo '$foo'
# prints $foo

运行时变量

Bash同样支持创建函数与传递参数

mcd () {
    mkdir -p "$1"
    cd "$1"
}

这里的$1表示函数的第一个参数。Bash中有很多这类变量,如下是一些常用的特殊变量:

  • $0 脚本的名字
  • $1 to $9 参数
  • $@ 全部的参数
  • $# 参数的数量
  • $? 上个命令的返回值
  • $$ 当前脚本的进程标志数字
  • !! 上一次运行的完整命令行,常用的命令例如sudo !!
  • $_ 上一次运行的命令行的最后一个参数

返回值 与 条件运算符

一般来说,命令行的使用STDOUT打印输出,STDERR打印错误,一个返回值来报告脚本运行状态

返回值0表示一切正常,非0值表示有错误发生。这些值可以被&&||连接进行条件运算。另外,处在同一行中的命令可以使用;分割为独立的部分。true表示0,false将表示1

false || echo "Oops, fail"
# Oops, fail

true || echo "Will not be printed"
#

true && echo "Things went well"
# Things went well

false && echo "Will not be printed"
#

false ; echo "This will always run"
# This will always run

替换模式

一个常见的情景是将一个命令行或脚本的输出作为变量。命令行替换(command substitution)可以完成这个工作。当命令行中出现$(CMD)时,Bash将执行CMD,得到输出并原地替换,例如for file in $(ls)

另一个不太为人熟知的是过程替换(process subsstitution),<(CMD)将会执行CMD并将输出保存至一个临时文件中,并将自己替换为文件名。对于通过文件而不通过STDIN传递参数的命令极为实用,例如diff <(ls foo) <(ls bar)

比较

下面的例子将会检查所有输入的文件,并给没有foobar的文件末尾加入foobar

#!/bin/bash

echo "Starting program at $(date)" # Date will be substituted

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

例子中将$?与0进行比较。事实上Bash支持很多这类操作,详见man test

当进行比较时,尽可能使用[[ ]]而不是[ ]。这样出错的机率更低,尽管这并不能直接移植到sh上。具体原因见这里

命令行展开

在执行命令行时,常常遇到需要输入相似的参数的时刻。Bash提供了一种方便的方式去拓展你的输入。

  • 野卡(wildcards)当你需要模糊匹配的时候使用?*类匹配一个或多个不确定的字符
  • 大括号(Curly braces){}当你的参数拥有共同的字串时可以自动展开为每个参数
convert image.{png,jpg}
# Will expand to
convert image.png image.jpg

cp /path/to/project/{foo,bar,baz}.sh /newpath
# Will expand to
cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath

# Globbing techniques can also be combined
mv *{.py,.sh} folder
# Will move all *.py and *.sh files


mkdir foo bar
# This creates files foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h
touch {foo,bar}/{a..h}
touch foo/x bar/y
# Show differences between files in foo and bar
diff <(ls foo) <(ls bar)
# Outputs
# < x
# ---
# > y

写shell是不太直观的,这里有个工具可以帮你找出错误

Shebang line

不是所有的从命令行里运行的脚本都得是Bash脚本。

#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)

该例子的第一行便是shebang line,指出了该脚本需要的解释器

函数与脚本的区别

  • 函数在shell中必须是相同的语言,而脚本可以用任何语言完成。
  • 函数只会被加载一次,当他们的定义被读入时。脚本则会在每次执行时被加载。所以当需要复用时,函数会稍快一点。
  • 函数运行在当前shell的环境中,而脚本运行于独立的进程中。也就是说函数可以改变当前shell的环境变量,而脚本不能。脚本需要使用export传递环境变量。

Shell工具

找到命令的用法

  • -h or --help
  • man
  • TLDR

找到文件

find是个类UNIX系统都有的非常好的工具

# Find all directories named src
find . -name src -type d
# Find all python files that have a folder named test in their path
find . -path '**/test/**/*.py' -type f
# Find all files modified in the last day
find . -mtime -1
# Find all zip files with size in range 500k to 10M
find . -size +500k -size -10M -name '*.tar.gz'

还可以执行命令

# Delete all files with .tmp extension
find . -name '*.tmp' -exec rm {} \;
# Find all PNG files and convert them to JPG
find . -name '*.png' -exec convert {} {.}.jpg \;

虽然find被大量使用,但是参数还是太复杂了,例如find -name '*PATTERN*'是严格match,-iname是大小写不敏感系列。输出也不好看。这里是一个好用的替代。此外还有一些基于数据库的locate可以使用,具体参见手册。

找到代码

grep用于在输入的文本中找到对应的模式。-C可以用于输出匹配行的上下文信息,-v用于反向(inverting)匹配(打印出没有匹配上的行)。

但是grep对搜索大量文件还是功能不足,比如跳不过.git文件夹。作者推荐用ripgrep作为替代。

# Find all python files where I used the requests library
rg -t py 'import requests'
# Find all files (including hidden files) without a shebang line
rg -u --files-without-match "^#!"
# Find all matches of foo and print the following 5 lines
rg foo -A 5
# Print statistics of matches (# of matched lines and files )
rg --stats PATTERN

找到Shell指令

一般来说在shell历史中查找用history | grep find就可以了。在大部分shell中Ctrl+R可以在历史中反向搜索。按下组合键之后输入要搜索的命令的子串之后,反复使用该组合键可以循环浏览能够匹配上子串的命令。该组合键与fzf搭配使用效果更佳。

还有一个相关功能是基于历史shell的自动补全。zsh内已经带了这个功能。

还有一点,若以空格开始一条命令,则这条命令不会被加入history。对于要输入敏感信息的命令非常有用

目录导航

用静态链接ln -s建立文件夹之间的快速跳转已经是老生常谈了,但是这对于快速访问相关文件是不够的。fasd是一个提供最近访问文件的工具,根据最近与频率进行排序的算法

Missing Semesternotes

Missing Semester Notes - Editor(Vim)

comments powered by Disqus