Bash 是一种 Shell。其他的 Shell 还有 sh, zsh 等。由于它是 Linux 中最为常见的 Shell,所以我深入学习了它
关于 Shell 的系统学习可参考这个网站:The Linux Command Line 中文版
- 获取命令帮助
- bash 自动补全
- 调试
- Expansion
- 重定向
- Readline
- 字符串(Strings)
- 数组(Arrays)
- 算术运算
- 输入输出(IO)
- 路径相关
- 进制转换
- 管道
- 捕获信号
- 自定义环境
- 遇到过的问题
- 转换 Windows 风格的换行符为 Linux 风格
- 将 ls 的输出赋值给 Arrays 变量
- How do I delete a file whose name begins with “-” (hyphen a.k.a. dash or minus)?
- How to find encoding of a file via script on Linux?
- How can I store the “find” command results as an array in Bash
- Split string into an array in Bash
- How to exclude a directory in
find .
command - How to execute a bash command stored as a string with quotes and asterisk
- How to force cp to overwrite without confirmation
- Is there a command to get the maximum folder depth of entire system?
- How to evaluate a boolean variable in an if block in bash?
- 实践记录
- 链接
获取命令帮助
三个主要的方法: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
表示文件(文件名),...
表示可以有多个文件,[]
里面的内容是可选的(即OPTIONS和FILE这两个参数都是可选的,如果不给出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.l
和flex --verbose a.l
等价。
此外,有的选项需要传入参数,如-o
:
1
-o, --outfile=FILE specify output filename
这时,我们需要传入FILE
这个参数,如:
1
flex -o a.c a.l
其中-o
和a.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
手册,这时可使用如下命令搜索并安装:
- 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
),但是笔者并不推荐,因为它不够新、翻译可能有误、不利于我们养成阅读英文文献的好习惯 - 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 --help
、man info
和info 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 Sheet 和 Readline(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,有少量修改
注意事项:
- 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)
- 在 Here Documents 中某些工具可能无法使用
- 用于表示终止的
LimitString
,必须开始于一行中的第一个字符处,不能有前导空格 - 建议使用多字符的
LimitString
- 对于交互过于复杂的应用,请使用
expect
用于需要标准输入的各种命令:ftp
、cat
、ex
、wall
等。
使用形式:
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
)
用途:
- 注释代码块
- 构建具有自说明文档的脚本(更好的注释)
例子:
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
使用echo
或printf
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/exeIf 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
,^A
(Ctrl+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-q
和C-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 dirname
和man 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
自定义环境
列出所有自定义函数?
列出所有函数:
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]\+='"
遇到过的问题
- string - Extract filename and extension in Bash - Stack Overflow
- string - How to trim whitespace from a Bash variable? - Stack Overflow
- linux - How to find out line-endings in a text file? - Stack Overflow
- macos - Executing Vim commands in a shell script - Stack Overflow
- string - How to trim whitespace from a Bash variable? - Stack Overflow
- How to Change / Set up bash custom prompt (PS1) in Linux - nixCraft
- shell - How to generate random number in Bash? - Stack Overflow
- Check if a Bash array contains a value - Stack Overflow
- Executing a Bash Script Function with Sudo - Unix & Linux Stack Exchange
- password - How to generate a random string? - Unix & Linux Stack Exchange
- string - Extract filename and extension in Bash - Stack Overflow
- linux - bash - find string index position of substring - Super User
- bash - Parsing JSON with Unix tools - Stack Overflow
转换 Windows 风格的换行符为 Linux 风格
Windows 的文本文件使用的换行符是\r\n
(CRLF);Linux 的是\n
(LF)。此外,MacOS 以前使用的是\r
(CR),不过现在也和 Unix 一样了。而转换方法有很多,如使用vim
、tr
、sed
、dos2unix
。这部分内容主要参考自: linux - How to convert DOS/Windows newline (CRLF) to Unix newline (LF) in a Bash script? - Stack Overflow和 HowTo: 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
dos2unix
和unix2dos
命令均来自包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
中使用,可以通过下面的方法输入特殊字符\r
:press 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 ./-
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
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
链接
下面总结了本文中使用的所有链接:
- How to read from a file or stdin in Bash? - Stack Overflow
- Sed使用笔记 - 处理缩略语
- The Linux Command Line 中文版
- shell - Bash: retrieve absolute path given relative - Stack Overflow
- shell - How to concatenate string variables in Bash - Stack Overflow