Bash使用笔记

Posted by wsxq2 on 2018-11-05
TAGS:  BashShell

本文最后一次编辑时间:2021-08-22 23:05:39 +0800

Bash 是一种 Shell。其他的 Shell 还有 sh, zsh 等。由于它是 Linux 中最为常见的 Shell,所以我深入学习了它

关于 Shell 的系统学习可参考这个网站:The Linux Command Line 中文版

获取命令帮助

三个主要的方法:COMMAND --help, man COMMAND, info COMMAND。且文档详细程度:info>man>--help。当然,个别命令可能详细程度相同

以上是针对普通命令或者叫做外部命令,对于 Shell(如 Bash) 内置命令,应使用help COMMAND来获取帮助

help

该命令是 Shell 内置命令,用于获取 Shell(如 Bash) 内置命令的帮助,如:

1
2
3
4
5
6
7
8
help
help ulimit
help for
help if
help [[
help case
help while
...

--help

下面以flex --help为例,详细说明如何使用--help

flex --help的输出如下:

Usage: flex [OPTIONS] [FILE]...
Generates programs that perform pattern-matching on text.

Table Compression:
  -Ca, --align      trade off larger tables for better memory alignment
  -Ce, --ecs        construct equivalence classes
  -Cf               do not compress tables; use -f representation
  -CF               do not compress tables; use -F representation
  -Cm, --meta-ecs   construct meta-equivalence classes
  -Cr, --read       use read() instead of stdio for scanner input
  -f, --full        generate fast, large scanner. Same as -Cfr
  -F, --fast        use alternate table representation. Same as -CFr
  -Cem              default compression (same as --ecs --meta-ecs)

Debugging:
  -d, --debug             enable debug mode in scanner
  -b, --backup            write backing-up information to lex.backup
  -p, --perf-report       write performance report to stderr
  -s, --nodefault         suppress default rule to ECHO unmatched text
  -T, --trace             flex should run in trace mode
  -w, --nowarn            do not generate warnings
  -v, --verbose           write summary of scanner statistics to stdout

Files:
  -o, --outfile=FILE      specify output filename
  -S, --skel=FILE         specify skeleton file
  -t, --stdout            write scanner on stdout instead of lex.yy.c
      --yyclass=NAME      name of C++ class
      --header-file=FILE   create a C header file in addition to the scanner
      --tables-file[=FILE] write tables to FILE

Scanner behavior:
  -7, --7bit              generate 7-bit scanner
  -8, --8bit              generate 8-bit scanner
  -B, --batch             generate batch scanner (opposite of -I)
  -i, --case-insensitive  ignore case in patterns
  -l, --lex-compat        maximal compatibility with original lex
  -X, --posix-compat      maximal compatibility with POSIX lex
  -I, --interactive       generate interactive scanner (opposite of -B)
      --yylineno          track line count in yylineno

Generated code:
  -+,  --c++               generate C++ scanner class
  -Dmacro[=defn]           #define macro defn  (default defn is '1')
  -L,  --noline            suppress #line directives in scanner
  -P,  --prefix=STRING     use STRING as prefix instead of "yy"
  -R,  --reentrant         generate a reentrant C scanner
       --bison-bridge      scanner for bison pure parser.
       --bison-locations   include yylloc support.
       --stdinit           initialize yyin/yyout to stdin/stdout
       --noansi-definitions old-style function definitions
       --noansi-prototypes  empty parameter list in prototypes
       --nounistd          do not include <unistd.h>
       --noFUNCTION        do not generate a particular FUNCTION

Miscellaneous:
  -c                      do-nothing POSIX option
  -n                      do-nothing POSIX option
  -?
  -h, --help              produce this help message
  -V, --version           report flex version

--help的输出通常以Usage: 开头,这部分概括了该命令的大致用法,如flex --help的这部分内容为(#后面的内容为注释):

1
Usage: flex [OPTIONS] [FILE]...

其中OPTIONS表示选项,FILE表示文件(文件名),...表示可以有多个文件,[]里面的内容是可选的(即OPTIONSFILE这两个参数都是可选的,如果不给出FILE这个参数的话,默认从标准输入(stdin读取)。从这里我们可以得知,该命令可以这样使用:

1
flex #从`stdin`读取,即需要手动输入 .l 文件的内容

也可以这样使用:

1
flex a.l

还可以使用选项:

1
flex -o a.c a.l

--help的输出的第二行通常为该命令的简要描述,flex的是:

1
Generates programs that perform pattern-matching on text.

从这里我们可以得知,flex用于生成在文本上执行模式匹配的程序

之后的内容便是选项说明(甚至会分类)。Linux 及类 Unix 中的选项分为长选项短选项,通常而言,每个选项都有长选项形式,而其中常用的选项会有短选项形式,短选项和相应的长选项等价(Windows CMD 没有这样的区分)。例如,如对于短选项-v(在分类Debugging下):

1
  -v, --verbose           write summary of scanner statistics to stdout

其对应的长选项为--verbose。使用时flex -v a.lflex --verbose a.l等价。

此外,有的选项需要传入参数,如-o

1
  -o, --outfile=FILE      specify output filename

这时,我们需要传入FILE这个参数,如:

1
flex -o a.c a.l

其中-oa.c之间的空格是可以省略的:

1
flex -oa.c a.l

同时,它和下面的用法等价:

1
flex --outfile=a.c a.l

此外,--help的输出中通常必定会有如下内容:

1
2
  -h, --help              produce this help message
  -V, --version           report xxx version

即对于任何命令,-h, --help-V, --version总是可用的(有的不支持-h,有的不支持--help,试试就知道了)

另外,表示选项结束可以使用 --(即后面都是其它参数),如:

1
2
3
4
5
6
7
root@master:sj# ls
a  a.c  -a.l  a.l  a.out  a.yy.c  from-info-flex.l  lex.yy.c  Makefile  parser-generator
root@master:sj# flex -a.l
flex: Unrecognized option `a'
Try `flex --help' for more information.
root@master:sj# flex -- -a.l
root@master:sj#

man

温馨提示:有的 Linux 发行版默认情况下可能没有安装man手册,这时可使用如下命令搜索并安装:

  1. CentOS:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    root@master:~# yum search man pages
    Loaded plugins: fastestmirror
    Loading mirror speeds from cached hostfile
      ...
    man-db.x86_64 : Tools for searching and reading man pages
    man-pages.noarch : Man (manual) pages from the Linux Documentation Project
    ...
    man-pages-zh-CN.noarch : Chinese Man Pages from Chinese Man Pages Project
    ...
    root@master:~# yum install man-pages
    

    注意其中还有中文手册(即man-pages-zh-CN),但是笔者并不推荐,因为它不够新、翻译可能有误、不利于我们养成阅读英文文献的好习惯

  2. Ubuntu:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    wsxq2@ubuntu-server:~$ apt search man pages -n
    Sorting... Done
    Full Text Search... Done
    ...
    manpages/xenial,xenial,now 4.04-2 all [installed]
      Manual pages about using a GNU/Linux system
    ...
    manpages-dev/xenial,xenial,now 4.04-2 all [installed,automatic]
      Manual pages about using GNU/Linux for development
    ...
    manpages-posix/xenial,xenial 2013a-1 all
      Manual pages about using POSIX system
       
    manpages-posix-dev/xenial,xenial 2013a-1 all
      Manual pages about using a POSIX system for development
    ...
    manpages-zh/xenial,xenial 1.5.2-1.1 all
      Chinese manual pages
    ...
    wsxq2@ubuntu-server:~$ apt install manpages manpages-dev manpages-posix manpages-posix-dev
    

    同样地,其中的中文手册(manpages-zh)不建议安装

man是除了--help使用最多的帮助命令。我们可以使用如下命令查看man命令的帮助:

1
man --help

可以看到,其用法如下:

1
Usage: man [OPTION...] [SECTION] PAGE...

即只有PAGE参数是必需的。PAGE参数即你要看的页面,通常为命令名,还可以为其它东西,如标准 C 的函数名,甚至配置文件等。例如:

1
2
3
4
man man
man flex
man sprinf
man resolv.conf #resolv.conf是Linux下配置DNS的文件

在使用man来查看帮助前,我们应当先阅读man man以学会使用man命令。这里就不详细说明了(因为man man里的内容已经足够详细了)。下面列出它常用的几个参数:

1
2
3
4
5
6
7
8
9
10
11
  -a, --all                  find all matching manual pages
  -w, --where, --path, --location
                             print physical location of man page(s)
  -P, --pager=PAGER          use program PAGER to display output
  -f, --whatis               equivalent to whatis
  -k, --apropos              equivalent to apropos
  -K, --global-apropos       search for text in all pages
  -l, --local-file           interpret PAGE argument(s) as local filename(s)
  -L, --locale=LOCALE        define the locale for this particular man search
  -M, --manpath=PATH         set search path for manual pages to PATH
  -E, --encoding=ENCODING    use selected output encoding

它的常用用法如下:

1
2
3
man -aw PAGE
man -P less PAGE
man -L zh-cn PAGE #使用yum install man-pages-zh-CN.noarch命令安装中文手册,但是并不推荐,因为中文不够新,且可能有翻译错误

info

info的内容非常详细,它是有目录的,整理得比较合理。强烈推荐使用(反正我是后悔没有早早地学会它)。关于info的使用可以参见info --helpman infoinfo info。这里只给出info中常用的快捷键(它的风格和vim很不像,且各个平台可能有所不同):

C-g         Cancel the current operation.

l           Close this help window.
q           Quit Info altogether.
H           Invoke the Info tutorial.

Up          Move up one line.
Down        Move down one line.
DEL         Scroll backward one screenful.
SPC         Scroll forward one screenful.

TAB         Skip to the next hypertext link.
RET         Follow the hypertext link under the cursor.
l           Go back to the last node seen in this window.

[           Go to the previous node in the document.
]           Go to the next node in the document.
p           Go to the previous node on this level.
n           Go to the next node on this level.
u           Go up one level.
t           Go to the top node of this document.

m           Pick a menu item specified by name.
g           Go to a node specified by name.

s           Search forward for a specified string.
{           Search for previous occurrence.
}           Search for next occurrence.

注意info的部分快捷键好像不是固定的,要注意随机应变

bash 自动补全

使用包管理器安装

1
2
yum install bash-completion -y # for CentOS
apt install bash-completion -y # for Ubuntu

从源码安装

Releases · scop/bash-completion 页面下载最新发布版本(当前是 2.9),然后解压:

1
tar xf bash-completion-2.9.tar.xz

安装:

1
2
3
4
5
6
cd bash-completion-2.9
autoreconf -i  # if not installing from prepared release tarball
./configure
make
make check # optional, requires python3 with pytest >= 3.6 and pexpect, dejagnu, and tcllib
make install # as root

然后在你的~/.bashrc中添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# bash_completion
function bash_completion()
{
	# Check for interactive bash and that we haven't already been sourced.
	[ -z "$BASH_VERSION" -o -z "$PS1" -o -n "$BASH_COMPLETION_COMPAT_DIR" ] && return

	# Check for recent enough version of bash.
	bash=${BASH_VERSION%.*}; bmajor=${bash%.*}; bminor=${bash#*.}

	if [ $bmajor -gt 4 ] || [ $bmajor -eq 4 -a $bminor -ge 1 ]; then
		[ -r "${XDG_CONFIG_HOME:-$HOME/.config}/bash_completion" ] && \
			. "${XDG_CONFIG_HOME:-$HOME/.config}/bash_completion"

        bash_completion_script=$([[ -r /usr/share/bash-completion/bash_completion ]] && \
            echo /usr/share/bash-completion/bash_completion || \
            echo /usr/local/share/bash-completion/bash_completion)
        if shopt -q progcomp && [ -r $bash_completion_script ]; then
            # Source completion code.
            . $bash_completion_script
        fi

        if [[ -x /etc/bash_completion.d ]];then
            for x in /etc/bash_completion.d/*;do
                . $x
            done
        fi
	fi
	unset bash bmajor bminor
}
bash_completion

然后就可以使用各种命令的自动补全了,比如openssl

1
2
3
4
5
6
7
8
9
10
11
12
13
root@master:~# openssl <tab>
Display all 101 possibilities? (y or n)
aes-128-cbc       bf-ecb            cast5-cfb         des-ecb           dgst              gendsa            pkcs8             rc2-ecb           sha1              version
aes-128-ecb       bf-ofb            cast5-ecb         des-ede           dh                genpkey           pkey              rc2-ofb           sha224            x509
aes-192-cbc       ca                cast5-ofb         des-ede3          dhparam           genrsa            pkeyparam         rc4               sha256
aes-192-ecb       camellia-128-cbc  cast-cbc          des-ede3-cbc      dsa               md2               pkeyutl           rc4-40            sha384
aes-256-cbc       camellia-128-ecb  ciphers           des-ede3-cfb      dsaparam          md4               prime             req               sha512
aes-256-ecb       camellia-192-cbc  crl               des-ede3-ofb      ec                md5               rand              rmd160            smime
asn1parse         camellia-192-ecb  crl2pkcs7         des-ede-cbc       ecparam           nseq              rc2               rsa               speed
base64            camellia-256-cbc  des               des-ede-cfb       enc               ocsp              rc2-40-cbc        rsautl            spkac
bf                camellia-256-ecb  des3              des-ede-ofb       engine            passwd            rc2-64-cbc        s_client          s_server
bf-cbc            cast              des-cbc           des-ofb           errstr            pkcs12            rc2-cbc           sess_id           s_time
bf-cfb            cast5-cbc         des-cfb           desx              gendh             pkcs7             rc2-cfb           sha               verify

又比如yum

1
2
3
4
root@master:_posts# yum <tab>
check             deplist           groups            info              load-transaction  reinstall         search            upgrade
check-update      distro-sync       help              install           makecache         remove            shell             version
clean             downgrade         history           list              provides          repolist          update

调试

追踪bash启动时加载了哪些文件(可以使用关键字bash startup trace谷歌搜索):

1
echo exit | strace bash -li |& grep '^open[a-z]*'

我的CentOS 7运行的结果如下(去掉了非配置文件的内容):

1
2
3
4
5
6
7
8
9
open("/etc/profile", O_RDONLY)          = 3
open("/etc/profile.d/*", O_RDONLY) = 3
open("/root/.bash_profile", O_RDONLY)   = 3
open("/root/.bashrc", O_RDONLY)         = 3
open("/usr/local/share/bash-completion/bash_completion", O_RDONLY) = 3
open("/root/.bash_history", O_RDONLY)   = 3
open("/root/.inputrc", O_RDONLY)        = 3
open("/root/.bash_logout", O_RDONLY)    = 3
open("/etc/bash.bash_logout", O_RDONLY) = -1 ENOENT (No such file or directory)

详情参见 profile - Find out what scripts are being run by bash on startup - Unix & Linux Stack Exchange

关于 bash 加载配置文件的顺序可参见 Bash Startup Files (Bash Reference Manual)

调试

1
2
3
4
5
6
# 当命令返回非 0 或使用未设置变量时强行退出
set -eu

# 调试开关(显示执行的命令)
set -x

Expansion

参见man bash中的^EXPANSION部分(使用/搜索)。其中包括但不限于以下内容:

  • ?*: 通配符
  • ~: 家目录
  • $(()): 数学运算
  • {}: 如ls abc{a,b,c}
  • $variable引用
  • $(): 执行命令,并返回它的标准输出
  • ': 单引号
  • ": 双引号
  • \x: 转义字符

还可参考 Expansion

重定向

参见man bash中的^REDIRECTION部分(使用/搜索)

Readline

Readline 用于处理交互式 Shell 的输入,充分利用可以使得输入命令的效率大大提高。但由于本身非常复杂,所以选择重点的常用的记住并反复使用即可。参见 Readline(Emacs) Cheat SheetReadline(vi) Cheat Sheet

详情参见man bash中的^READLINE部分(使用/搜索)

字符串(Strings)

拼接字符串

本部分内容参考自 shell - How to concatenate string variables in Bash - Stack Overflow

使用 ""

1
2
3
4
5
6
7
8
9
$ foo="Hello"
$ foo="$foo World"
$ echo $foo
Hello World
$ a='hello'
$ b='world'
$ c="$a$b"
$ echo $c
helloworld

使用 +=

1
2
3
4
5
6
7
8
$ A="X Y"
$ A+=" Z"
$ echo "$A"
X Y Z
$ a=2
$ a+=4
$ echo $a
24

使用 printf

1
2
3
4
$ foo="Hello"
$ printf -v foo "%s World" $foo
$ echo $foo
Hello World

字符串匹配通配符

=

参见 使用=

case..in..

参见 使用case..in..

字符串匹配正则表达式

该部分参考自 Check if a string matches a regex in Bash script - Stack Overflow

=~

1
[[ $date =~ ^[0-9]{8}$ ]] && echo "yes"

expr match

1
expr match "$date" "^[0-9]\{8\}" >/dev/null && echo yes

数组(Arrays)

Bash 支持一维数组(数字索引和字符串索引),关于 Bash 中数组的更多知识,可参见info bash 'bash feature' array(也可以参见man bash)。下面即是引用自man bash中的部分内容:

Bash provides one-dimensional indexed and associative array variables. Any variable may be used as an indexed array; the declare builtin will explicitly declare an array. There is no maximum limit on the size of an array, nor any requirement that members be indexed or assigned contiguously. Indexed arrays are referenced using integers (including arithmetic expressions) and are zero-based; associative arrays are referenced using arbitrary strings.

——引用自man bash的 Arrays 部分

索引数组(indexed array)

创建方法如下:

1
2
declare -a a #可选
a=(a b c d)

引用时使用${a[0]}${a[1]}……即可。此外,使用${a[@]}可以获得数组的所有成员(注意和${a[*])}有所区别);使用${#a[0]}可以获得 a[0] 的长度,类似地,使用${#a[@]}可以获得数组 a 的长度

以下是一些示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
root@master:~# IA=(a bs cadfa)
root@master:~# echo ${IA[0]}
a
root@master:~# echo ${IA[1]}
bs
root@master:~# echo ${IA[@]}
a bs cadfa
root@master:~# echo ${#IA[@]}
3
root@master:~# echo ${IA[*]}
a bs cadfa
root@master:~# for i in ${IA[@]}; do
> echo $i
> done
a
bs
cadfa
root@master:~# IA=([12]=a [0]=b)
root@master:~# echo ${IA[1]}

root@master:~# echo ${IA[0]}
b
root@master:~# echo ${IA[12]}
a
root@master:~# echo ${#IA[@]}
2

关联数组(associative array)

创建方法如下:

1
declare -A a=(['a']=1 ['b']=2 ['c']=3 ['d']=4) #这里必需使用`declare -A`,否则会创建为 indexed array

引用时使用${a["a"]}${a["b"]}……即可。此外,使用${a[@]}可以获得关联数组的所有 values,使用${!a[@]}可以获得关联数组的所有 keys;使用${#a['a']}可以获得 a[‘a’] 的长度,类似地,使用${#a[@]}可以获得关联数组 a 的长度

以下是一些示例:

root@master:~# declare -A AA=(['a']=1 ['b']=2 ['c']=3 ['d']=4)
root@master:~# AA['e']=5
root@master:~# echo ${#AA[@]}
5
root@master:~# echo ${AA['e']}
5
root@master:~# echo ${AA[@]}
1 2 3 4 5
root@master:~# echo ${!AA[@]}
a b c d e
root@master:~# for i in "${!AA[@]}"
> do
>   echo "key: $i, value: ${AA[$i]}"
> done
key: a, value: 1
key: b, value: 2
key: c, value: 3
key: d, value: 4
key: e, value: 5

算术运算

整数运算

$(())

1
2
$ echo "$((5*5+5-3/2))"
29

expr

1
2
$ expr 5 - 4
1

浮点运算

bc

1
2
$ echo "5.01-4*2.0"|bc
-2.99

awk

1
2
$ awk 'BEGIN{print 7.01*5-4.01}'
31.04

输入输出(IO)

从文件或stdin中读取输入

1
2
3
4
while read line
do
  echo "$line"
done < "${1:-/dev/stdin}"

以上脚本实现:执行该脚本时将第一个参数视为文件名,从该文件中读取输入;如果没有传入参数,则从/dev/stdin(即标准输入读取输入)

${1:-/dev/stdin}根据条件执行替换,即如果$1(传入脚本的第一个参数)不存在则由/dev/stdin替换。

详情参见 How to read from a file or stdin in Bash? - Stack Overflow

Here Documents

后文主要来自 Here Documents,有少量修改

注意事项

  1. Here documents 创建临时文件,但是这些文件在打开后会被立即删除,并且无法通过其它进程访问:
    1
    2
    3
    
    bash$ bash -c 'lsof -a -p $$ -d0' << EOF
    > EOF
    lsof    1213 bozo    0r   REG    3,5    0 30386 /tmp/t1213-0-sh (deleted)
    
  2. 在 Here Documents 中某些工具可能无法使用
  3. 用于表示终止的LimitString,必须开始于一行中的第一个字符处,不能有前导空格
  4. 建议使用多字符的LimitString
  5. 对于交互过于复杂的应用,请使用expect

用于需要标准输入的各种命令:ftpcatexwall等。

使用形式:

1
2
3
4
5
6
interactive-program <<LimitString
command #1
command #2
...
LimitString

简单例子

broadcast: Sends message to everyone logged in
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

wall <<zzz23EndOfMessagezzz23
E-mail your noontime orders for pizza to the system administrator.
    (Add an extra dollar for anchovy or mushroom topping.)
# Additional message text goes here.
# Note: 'wall' prints comment lines.
zzz23EndOfMessagezzz23

# Could have been done more efficiently by
#         wall <message-file
#  However, embedding the message template in a script
#+ is a quick-and-dirty one-off solution.
dummyfile: Creates a 2-line dummy file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash

# Noninteractive use of 'vi' to edit a file.
# Emulates 'sed'.

E_BADARGS=85

if [ -z "$1" ]
then
  echo "Usage: `basename $0` filename"
  exit $E_BADARGS
fi

TARGETFILE=$1

# Insert 2 lines in file, then save.
#--------Begin here document-----------#
vi $TARGETFILE <<x23LimitStringx23
i
This is line 1 of the example file.
This is line 2 of the example file.
^[
ZZ
x23LimitStringx23
#----------End here document-----------#

#  Note that ^[ above is a literal escape
#+ typed by Control-V <Esc>.

#  Bram Moolenaar points out that this may not work with 'vim'
#+ because of possible problems with terminal interaction.

exit
Using ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
#  Replace all instances of "Smith" with "Jones"
#+ in files with a ".txt" filename suffix. 

ORIGINAL=Smith
REPLACEMENT=Jones

for word in $(fgrep -l $ORIGINAL *.txt)
do
  # -------------------------------------
  ex $word <<EOF
  :%s/$ORIGINAL/$REPLACEMENT/g
  :wq
EOF
  # :%s is the "ex" substitution command.
  # :wq is write-and-quit.
  # -------------------------------------
done
Multi-line message using cat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash

#  'echo' is fine for printing single line messages,
#+  but somewhat problematic for for message blocks.
#   A 'cat' here document overcomes this limitation.

cat <<End-of-message
-------------------------------------
This is line 1 of the message.
This is line 2 of the message.
This is line 3 of the message.
This is line 4 of the message.
This is the last line of the message.
-------------------------------------
End-of-message

#  Replacing line 7, above, with
#+   cat > $Newfile <<End-of-message
#+       ^^^^^^^^^^
#+ writes the output to the file $Newfile, rather than to stdout.

exit 0


#--------------------------------------------
# Code below disabled, due to "exit 0" above.

# S.C. points out that the following also works.
echo "-------------------------------------
This is line 1 of the message.
This is line 2 of the message.
This is line 3 of the message.
This is line 4 of the message.
This is the last line of the message.
-------------------------------------"
# However, text may not include double quotes unless they are escaped.
Here document with replaceable parameters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash
# Another 'cat' here document, using parameter substitution.

# Try it with no command-line parameters,   ./scriptname
# Try it with one command-line parameter,   ./scriptname Mortimer
# Try it with one two-word quoted command-line parameter,
#                           ./scriptname "Mortimer Jones"

CMDLINEPARAM=1     #  Expect at least command-line parameter.

if [ $# -ge $CMDLINEPARAM ]
then
  NAME=$1          #  If more than one command-line param,
                   #+ then just take the first.
else
  NAME="John Doe"  #  Default, if no command-line parameter.
fi

RESPONDENT="the author of this fine script"


cat <<Endofmessage

Hello, there, $NAME.
Greetings to you, $NAME, from $RESPONDENT.

# This comment shows up in the output (why?).

Endofmessage

# Note that the blank lines show up in the output.
# So does the comment.

exit

suppresses leading tabs

使用-<<-LimitString

例子:

Example 19-4. Multi-line message, with tabs suppressed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
# Same as previous example, but...

#  The - option to a here document <<-
#+ suppresses leading tabs in the body of the document,
#+ but *not* spaces.

cat <<-ENDOFMESSAGE
	This is line 1 of the message.
	This is line 2 of the message.
	This is line 3 of the message.
	This is line 4 of the message.
	This is the last line of the message.
ENDOFMESSAGE
# The output of the script will be flush left.
# Leading tab in each line will not show.

# Above 5 lines of "message" prefaced by a tab, not spaces.
# Spaces not affected by   <<-  .

# Note that this option has no effect on *embedded* tabs.

exit 0

suppress parameter substitution

使用"<<"LimitString")或'<<'LimitString')或\<<\LimitString

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash
#  A 'cat' here-document, but with parameter substitution disabled.

NAME="John Doe"
RESPONDENT="the author of this fine script"  

cat <<'Endofmessage'

Hello, there, $NAME.
Greetings to you, $NAME, from $RESPONDENT.

Endofmessage

#   No parameter substitution when the "limit string" is quoted or escaped.
#   Either of the following at the head of the here document would have
#+  the same effect.
#   cat <<"Endofmessage"
#   cat <<\Endofmessage



#   And, likewise:

cat <<"SpecialCharTest"

Directory listing would follow
if limit string were not quoted.
`ls -l`

Arithmetic expansion would take place
if limit string were not quoted.
$((5 + 3))

A a single backslash would echo
if limit string were not quoted.
\\

SpecialCharTest


exit

anonymous

即使用空命令:: <<LimitString

用途:

  1. 注释代码块
  2. 构建具有自说明文档的脚本(更好的注释)

例子:

Example 19-10. “Anonymous” Here Document

1
2
3
4
5
6
7
#!/bin/bash

: <<TESTVARIABLES
${HOSTNAME?}${USER?}${MAIL?}  # Print error message if one of the variables not set.
TESTVARIABLES

exit $?

Example 19-11. Commenting out a block of code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# commentblock.sh
echo "Just before commented-out code block."
#  The lines of code between the double-dashed lines will not execute.
#  ===================================================================
: <<DEBUGXXX
for file in *
do
 cat "$file"
done
DEBUGXXX
#  ===================================================================
echo "Just after commented-out code block."

exit 0

Example 19-12. A self-documenting script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash
# self-document.sh: self-documenting script
# Modification of "colm.sh".

DOC_REQUEST=70

if [ "$1" = "-h"  -o "$1" = "--help" ]     # Request help.
then
  echo; echo "Usage: $0 [directory-name]"; echo
  sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATIONXX$/p' "$0" |
  sed -e '/DOCUMENTATIONXX$/d'; exit $DOC_REQUEST; fi


: <<DOCUMENTATIONXX
List the statistics of a specified directory in tabular format.
---------------------------------------------------------------
The command-line parameter gives the directory to be listed.
If no directory specified or directory specified cannot be read,
then list the current working directory.

DOCUMENTATIONXX

if [ -z "$1" -o ! -r "$1" ]
then
  directory=.
else
  directory="$1"
fi

echo "Listing of "$directory":"; echo
(printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
; ls -l "$directory" | sed 1d) | column -t

exit 0

输入 ASCII 特殊字符

该部分的测试环境如下:

  • 主机操作系统:Windows 10 1803
    • 使用的 SSH 工具:Putty 0.70
  • 虚拟机操作系统:CentOS 7.2
    • 使用的 Shell: Bash 4.2.46

使用echoprintf

1
echo -ne '\x05\x01\x00'|nc -x nc.log localhost 1080

或者:

1
echo -n $'\x05\x01\x00'|nc -x nc.log localhost 1080

不过上述方法中,后一种方法似乎无法输入\x00字符

printf和上述方法类似

使用python

If you don’t wan’t to create a file, here’s an alternative

1
python -c 'print("\x61\x62\x63\x64")' | /path/to/exe

If you want stdin control to be transferred back

1
( python -c 'print("\x61\x62\x63\x64")' ; cat ) | /path/to/exe

——引用自bash - Type characters in hexadecimal notation to standard input - Stack Overflow

使用重定向<

1
2
echo -ne '\x05\x01\x00\x04\x05\x01\x00\x03\x0e\x77\x77\x77\x2e\x67\x6f\x6f\x67\x6c\x65\x2e\x63\x6f\x6d\x01\xbb' >input
nc -x nc.log localhost 1080 < input

但是上述两种方法(echo<)都只能发送一次消息,因此实在让人难以满意。于是经过大量的搜索,我找到了如下解决方案

直接输入

如果是在标准输入状态下(例如使用了cat命令,需要从标准输入读取数据),可以直接参见 ASCII - Wikipedia 的那个表格,我们可以知道^@Ctrl+2)表示 ASCII 字符\x00^ACtrl+A)表示 ASCII 字符\x01,以此类推,可以轻松输入任意 ASCII 特殊字符

如果是在Bash中输入命令,则可以在上述方法中先加一个Ctrl+V键即可。例如你想要输入\x01,实际输入Ctrl+V,Ctrl+A即可。这个方法存在一个显著的问题,那就是\x00无法输入。此外还存在一个小问题,如\x127^?)不能通过Ctrl+V, Ctrl+/Ctrl+V, Ctrl+?输入,但是可以通过Ctrl+V, Ctrl+BackSpace(或者输入Ctrl+V, BackSpace)来输入。关于Bash中可用的用于改变文本的快捷键可参考man bash中的Commands for Changing Text部分,其中提到:

quoted-insert (C-q, C-v)

Add the next character typed to the line verbatim. This is how to insert characters like C-q, for example.

不过我觉得C-qC-v的顺序写反了

由于Bash中的快捷键是通过readline库实现的,所以更多信息可参考 The GNU Readline Library

常用的快捷键的参考手册(emacs模式):Readline Cheat Sheet

常用的快捷键的参考手册(vi模式):Readline Cheat Sheet

路径相关

转换相对路径为绝对路径

本部分参考自 shell - Bash: retrieve absolute path given relative - Stack Overflow

1
relative_path="../../program/py/hack/get_host.sh"

$PWD

1
echo "$PWD/$relative_path"

realpath

1
echo `realpath $relative_path`

dirname && basename

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dirname /usr/bin/
 -> "/usr"

dirname dir1/str dir2/str
 -> "dir1" followed by "dir2"

dirname stdio.h
 -> "."

basename /usr/bin/sort
 -> "sort"

basename include/stdio.h .h
 -> "stdio"

basename -s .h include/stdio.h
 -> "stdio"

basename -a any/str1 any/str2
 -> "str1" followed by "str2"

——引用自man dirnameman basename

进制转换

any base to decimal?

1
2
echo "obase=10; ibase=16; $hexNum" | bc
echo $((16#$hexNum))

其中第一种方法是使用了 bc,自不必多说。而后一种方法就很有意思了,它使用了 Bash Expansion 中的Arithmetic Expansion$((EXP))):

Constants with a leading 0 are interpreted as octal numbers. A leading 0x or 0X denotes hexadecimal. Otherwise, numbers take the form [base#]n, where the optional base is a decimal number between 2 and 64 representing the arithmetic base, and n is a number in that base. If base# is omitted, then base 10 is used. The digits greater than 9 are represented by the lowercase letters, the uppercase letters, @, and _, in that order. If base is less than or equal to 36, lowercase and uppercase letters may be used interchangeably to represent numbers between 10 and 35.

——引用自man bash中的ARITHMETIC EVALUATION部分(可使用/^AR快速到达)

详情参见 Convert Hexadecimal to Decimal in Bash – Linux Hint

hex number to binary string?

1
2
3
echo 'ibase=16;obase=2;5f' | bc
perl -e 'printf "%08b\n", 0x5D'
printf '\x5F' | xxd -b | cut -d' ' -f2

管道

grep through | can’t get stderr content?

gcc 的-v参数会将详细信息输出到标准错误stderr,直接使用管道得到的输入为空。如下所示:

1
2
3
4
5
6
root@master:tmp# gcc -v a.c | grep cc1
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
...

但是我们可以这样:

1
2
root@master:tmp# gcc -v a.c |& grep cc1
 /usr/libexec/gcc/x86_64-redhat-linux/4.8.5/cc1 -quiet -v a.c -quiet -dumpbase a.c -mtune=generic -march=x86-64 -auxbase a -version -o /tmp/cc92AIgc.s

其中|&2&1 |等价:

If & is used, the standard error of command is connected to command2’s standard input through the pipe; it is shorthand for 2>&1 . This implicit redirection of the standard error is performed after any redirections specified by the command

——引用自man bash(使用/|&快速跳转到相应位置)

捕获信号

响应ctrl+c

Try the following code :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# type "finish" to exit

# function called by trap
other_commands() {
    printf "\rSIGINT caught      "
    sleep 1
    printf "\rType a command >>> "
}

trap 'other_commands' SIGINT

input="$@"

while true; do
    printf "\rType a command >>> "
    read input
    [[ $input == finish ]] && break
    bash -c "$input"
done

——引用自 BASH - using trap ctrl+c - Stack Overflow

自定义环境

列出所有自定义函数?

列出所有函数:

1
declare -F

参见 bash - How do I list the functions defined in my shell? - Stack Overflow

列出所有自定义函数(并不准确):

1
declare -F |awk '{print $3}' | egrep '^[a-z0-9]{1,5}$'

列出所有自定义变量?

1
2
 # my environment
alias me="env | grep '^[a-z]\+='"

遇到过的问题

转换 Windows 风格的换行符为 Linux 风格

Windows 的文本文件使用的换行符是\r\n(CRLF);Linux 的是\n(LF)。此外,MacOS 以前使用的是\r(CR),不过现在也和 Unix 一样了。而转换方法有很多,如使用vimtrseddos2unix。这部分内容主要参考自: linux - How to convert DOS/Windows newline (CRLF) to Unix newline (LF) in a Bash script? - Stack OverflowHowTo: UNIX / Linux Convert DOS Newlines CR-LF to Unix/Linux Format - nixCraft

vim

Vim 作为一个极为强大的文本编辑器,处理这点小问题自然不在话下:

1
2
vim file.txt -c "set ff=unix" -c ":wq" # Windows to Linux
vim file.txt -c "set ff=dos" -c ":wq" # Linux to Windows

dos2unix

dos2unixunix2dos命令均来自包dos2unix,在 CentOS 中可以使用如下命令安装:

1
yum install dos2unix

然后可以使用如下方法使用它:

1
2
dos2unix <filename> #原地转换
dos2unix -n <input-file> <output-file> #生成新文件,保留副本

当然,也可以使用unix2dos反向转换

tr

tr是 translate 的缩写,即有转换翻译之意。它是 Linux 中的一个非常常用的小工具(属于软件包coreutils),用来删除或是转换全文中的某些字符。详情参见man tr

由于 Windows 和 Linux 换行符的差异在于 Windows 多了一个\r,所以删除它即可:

1
tr -d '\r' <infile >outfile

sed

sed是 Linux 中的一个文本流编辑器。使用它也可以删除\r字符从而达到目的

1
sed -e 's/\r//g' <infile >outfile

此外,如果是在交互式的bash中使用,可以通过下面的方法输入特殊字符\rpress Ctrl-V then Ctrl-M。如:

1
sed 's/^M$//' input.txt > output.txt

将 ls 的输出赋值给 Arrays 变量

本部分参考自 How do I assign ls to an array in Linux Bash? - Stack Overflow

Bash 支持一维数组(数字索引和字符串索引),关于 Bash 中数组的更多知识,可参见info bash 'bash feature' array(也可以参见man bash)。其中有提到给数组赋值的方法,即:

1
2
declare -a a #可选
a=(a b c d)

引用时使用${a[0]}${a[1]}……即可。此外,使用${a[@]}可以获得数组的所有成员(注意和${a[*])}有所区别);使用${#a[0]}可以获得 a[0] 的长度,类似地,使用${#a[@]}可以获得数组 a 的长度

因此,一个简单的方法是:

1
array=($(ls -d */))

事实上,直接这样就可以了:

1
array=(*/)

然而,上述方法不能很好地处理文件名中有特殊符号的情形(如空格)。因此稳健的方法如下:

1
2
3
4
5
6
7
shopt -s nullglob
array=(*/)
shopt -u nullglob # Turn off nullglob to make sure it doesn't interfere with anything later
echo "${array[@]}"  # Note double-quotes to avoid extra parsing of funny characters in filenames
if (( ${#array[@]} == 0 )); then
    echo "No subdirectories found" >&2
fi

How do I delete a file whose name begins with “-” (hyphen a.k.a. dash or minus)?

1
rm -- --help

1
rm ./--help

对于文件名为-的文件,可以这样删除:

1
rm ./-

详情参见 shell - How do I delete a file whose name begins with “-“ (hyphen a.k.a. dash or minus)? - Unix & Linux Stack Exchange

How to find encoding of a file via script on Linux?

1
file -bi <file name>

详情参见 shell - How to find encoding of a file via script on Linux? - Stack Overflow

How can I store the “find” command results as an array in Bash

简易版本(要求 Bash 4.4+):

1
readarray -d '' array < <(find . -name "$input" -print0)

有缺陷的版本:

1
readarray -t all_source_file < <(find . -path ./after_iconv -prune -o \( -name '*.cpp' -o -name '*.c' -o -name '*.h' -o -name '*.txt' \) -type f -print);

通用但复杂的版本:

1
2
3
4
array=()
while IFS=  read -r -d $'\0'; do
    array+=("$REPLY")
done < <(find . -name "${input}" -print0)

详情参见 How can I store the “find” command results as an array in Bash - Stack Overflow

Split string into an array in Bash

1
2
3
string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
# declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

详情参见https://stackoverflow.com/a/45201229

How to exclude a directory in find . command

参见 https://stackoverflow.com/a/4210072

How to execute a bash command stored as a string with quotes and asterisk

1
eval $cmd

参见 scripting - How to execute a bash command stored as a string with quotes and asterisk - Stack Overflow

How to force cp to overwrite without confirmation

参见 https://stackoverflow.com/a/8488292

Is there a command to get the maximum folder depth of entire system?

1
find / -type d | sed 's|[^/]||g' | sort | tail -n1 | wc -c

上述结果 -1 即可,因为wc命令多算了一个换行符。如果需要准确的结果,可以使用如下命令:

1
find /etc/ -type d | sed 's|[^/]||g' | sort | tail -n1 | egrep -o / |wc -l

详情参见 macos - Is there a command to get the maximum folder depth of entire system? - Super User

How to evaluate a boolean variable in an if block in bash?

1
2
3
myVar=true
if $myVar ; then echo true ;else echo false; fi
if ! $myVar ; then echo true ;else echo false; fi

详情参见:How to evaluate a boolean variable in an if block in bash? - Stack Overflow

以及 help if

实践记录

处理缩略语

问题详情参见 Sed使用笔记 - 处理缩略语

使用grep

1
2
3
4
5
6
7
8
9
10
11
temp=$IFS
IFS=
while read line
do
	tmp=$(grep -h "^\*\[$line\]" abbreviations.txt)
	if [[ -z $tmp ]]; then
		tmp="*[$line]: "
	fi
	echo $tmp
done < "${1:-/dev/stdin}"
IFS=$temp

注意$()命令需要修改IFS以保证$tmp变量不会丢失换行符,详情参见: shell - Why do newline characters get lost when using command substitution? - Unix & Linux Stack Exchange

使用case..in..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while read word
do
	tmp=
	while read abbr
	do
		glob="\*\[$word\]:*"
		case $abbr in
			 $glob )
				if [[ $tmp ]]; then
					tmp="$tmp\n$abbr";
				else
					tmp=$abbr;
				fi
				;;
		esac
	done < abbreviations.txt
	if [[ -z $tmp ]]; then
		tmp="*[$word]: "
	fi
	echo -e $tmp
done < "${1:-/dev/stdin}"

使用=~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while read word
do
	tmp=
	while read abbr
	do
		regex="^\*\[$word]:.*\$"
		if [[ $abbr =~ $regex ]]; then
			if [[ $tmp ]]; then
				tmp="$tmp\n$abbr";
			else
				tmp=$abbr;
			fi
		fi
	done < abbreviations.txt
	if [[ -z $tmp ]]; then
		tmp="*[$word]: "
	fi
	echo -e $tmp
done < "${1:-/dev/stdin}"

使用=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while read word
do
	tmp=
	while read abbr
	do
		glob="\*\[$word\]:*"
		if [[ $abbr = $glob ]]; then
			if [[ $tmp ]]; then
				tmp="$tmp\n$abbr";
			else
				tmp=$abbr;
			fi
		fi
	done < abbreviations.txt
	if [[ -z $tmp ]]; then
		tmp="*[$word]: "
	fi
	echo -e $tmp
done < "${1:-/dev/stdin}"

将 Windows 格式的文本文件转换为 Linux 格式

每当我使用 Linux 中的 Vim 打开 Windows 上编辑的含有中文的文本文件时,它会在最下面出现如下提示(尤其是使用记事本):

1
"main.cpp" [noeol][converted][dos] 196L, 4767C

由此总结出 Windows 的文本文件和 Linux 中的文本文件在格式上的区别如下:

  • Windows 的文本文件可能没有eof;Linux 的通常都有。可以使用 sed 工具解决这个问题:
    1
    
    sed -e '$s/.*/&\n/' < infile > outfile
    
  • Windows 的文本文件编码通常为cp936;Linux 的则为UTF-8。可以使用iconv进行转换:
    1
    
    iconv -f CP936 -t UTF-8 < infile > outfile
    
  • Windows 的文本文件使用的换行符是\r\n(CRLF);Unix 的是\n(LF)。此外,MacOS 以前使用的是\r(CR),不过现在也和 Unix 一样了。可以使用tr工具删除多余的\r
    1
    
    tr -d '\r' < infile > outfile
    

从而写出了如下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Windows to Linux
w2u ()
{
    readarray -t all_source_file < <(find . -path ./after_iconv -prune -o \( -name '*.cpp' -o -name '*.c' -o -name '*.h' -o -name '*.txt' \) -type f -print);
    TMPFILE1="$(mktemp -t --suffix=.txt a_sh.XXXXXX)";
    TMPFILE2="$(mktemp -t --suffix=.txt a_sh.XXXXXX)";
    trap "rm -f '$TMPFILE1 $TMPFILE2'" 0;
    trap "rm -f '$TMPFILE1 $TMPFILE2'; exit 1" 2;
    trap "rm -f '$TMPFILE1 $TMPFILE2'; exit 1" 1 15;
    for f in "${all_source_file[@]}";
    do
        dir_of_f="after_iconv/$(dirname $f)";
        if [[ ! -d "$dir_of_f" ]]; then
            echo mkdiring $dir_of_f;
            mkdir -p $dir_of_f;
        fi;
        if [[ ! "$(file -b --mime-type $f)" = text/* ]]; then
            continue;
        fi;
        echo iconving $f to after_iconv/$f;
        \cp -af $f $TMPFILE1;
        encoding="$(file -b --mime-encoding $TMPFILE1)";
        if [[ ! "$encoding" = utf\-8 ]]; then
            iconv -f "$encoding" -t UTF-8 -o $TMPFILE2 $TMPFILE1;
            \cp -af $TMPFILE2 $TMPFILE1;
        fi;
        if [[ "$(file $TMPFILE1)" = *CRLF* ]]; then
            tr -d '\r' < $TMPFILE1 > $TMPFILE2;
            \cp -af $TMPFILE2 $TMPFILE1;
        fi;
        \cp -af $TMPFILE1 after_iconv/$f;
    done
}

它的功能是将当前目录下所有的文件名以.c.cpp.h.txt结尾的文件找出来,并将它们的格式从 Windows 转换为 Linux。以便在 Linux 中编译运行

事实上,可以直接使用 Vim 实现上述功能:

1
2
3
vim file.txt -c "set ff=unix" -c ":wq" # 处理换行符问题:将 \r\n 改为 \n
vim file.txt -c "set fenc=utf8" -c ":wq" # 处理文件编码问题:将 cp936 转换为 utf8
vim file.txt -c "$s/.*/&\n/" -c ":wq" # 处理 eof 问题:在最后一行后添加 \n。这一步好像不必要

不过使用 Vim 处理大量文件时效率比较低下,这点还需注意

dream 项目中的需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
declare will_exit=false
function prepare_exit(){
        echo "检测到 CTRL+C,程序将在完成本轮操作后退出……"
        will_exit=true
}

trap prepare_exit SIGINT

declare -i i=1
while true
do
        echo "第 $i 轮开始……"
        echo "远程执行脚本……"
        ssh -p26635 root@wsxq21.55555.io "cd ~/dream/ && python3 main.py"
        echo "下载到本地……"
        scp -P26635 root@wsxq21.55555.io:~/dream/videos/* /mnt/d/learn/
        echo "删除远程主机上的文件……"
        ssh -p26635 root@wsxq21.55555.io "rm -rf ~/dream/videos/*"
        echo "第 $i 轮完成!"
        [[ $will_exit ]] && break
        i+=1
done

链接

下面总结了本文中使用的所有链接: