Jade Dungeon

bash

bash基础

初始化配置文件

在不同的运行环境下,shell初始化的配置文件是不一样的。

一般来说bashrc是每次打开一个shell都会执行的配置,而名字里带profile的 配置文件都是只在用户第一次登录的时候执行一次,而且更新配置以后要重启才生效。

所以一般为了减少重复配置,会把二者共用的配置放到bashrc文件中, 再用profile配置来调用bashrc

  • /etc/profile:对所有用户生效,仅登录时执行一次。
  • ~/.bash_profile:仅对当前用户生效,仅登录时执行一次。
  • /etc/bashrc:对所有用户生效,每次开启bash交互或执行脚本都会执行。
  • ~/.bashrc:仅对当前用户生效,每次开启bash交互或执行脚本都会执行。

图例

  • 首先读入全局环境变量设定档/etc/profile
  • 然后根据其内容读取额外的设定的文档,如/etc/profile.d/etc/inputrc
  • 根据不同使用者帐号,于其家目录内读取~/.bash_profile; 读取失败则会读取~/.bash_login; 再次失败则读取~/.profile(这三个文档设定基本上无差别,仅读取上有优先关系);
  • 最后,根据用户帐号读取~/.bashrc

登录shell时的配置文件

登录shell是指在启动bash时加上了--login选项。

  • /etc/profile:此文件为系统的每个用户设置环境信息。当用户第一次登录时, 该文件被执行。并从/etc/profile.d目录的配置文件中搜集shell的设置。
  • ~/.bash_profile:每个用户的个性配置。当用户登录时,该文件仅仅执行一次。 默认情况下,他设置一些环境变量,执行用户的,.bashrc文件。
  • ~/.bash_login:用户登录后的设置
  • ~/.profile
  • ~/.bash_logout:用户登出时的特殊处理。

交互式非登录shell的配置文件

登录shell是指在启动bash时不加--login选项,则会调用完全不同的配置文件:

  • /etc/bashrc:为每一个运行bash shell的用户执行此文件。 当bash shell被打开时,该文件被读取。
  • ~/.bashrc:每个用户的个性化配置。当登录时以及每次打开新的shell时, 该文件被读取。

非交互式shell

  • BASH_ENV变量:非交互式shell会查找环境变量BASH_ENV, 并执行该变量指定的文件中的命令。

再次运行初始化文件source.

修改了配置以后不一定要重启,可以用source或是.来运行shell脚本文件。

如果不用source或是.来运行shell脚本文件, 所有的配置内容只会在当前的shell中生效。

符号命令

  • ():子shell。
  • $():命令替换。
  • (()):相当于let,是表达式计算。
  • $(()):算术扩展(不用于被 括起来的值中包含秸的情形)。
  • []:条件表达式,test命令,不能比较字符串。
  • [[]]:条件表达式,能比较字符串。

输入输出与重定向

  • < filename
  • > filename
  • >| filename:即使文件已经存在,并标记为noclobber,依然会被定向为标准输出。
  • >> filename:追加文件的方式重定向标准输出。
  • <&m:为文件描述符m复制标准输入。
  • [n]>&m:合并到文件描述符m,如果n不指定,就是1
  • [n]<&-:关闭文件描述符n,默认是0
  • [n]>&-:关闭文件描述符n,默认是1

合并文件描述符:&

&>可以合并文件描述符:

cat file.txt 1> out.txt 2>&1

上面的代码把标准输出重定向到文件out.txt,然后把标准错误合并到标准输出。 这样标准输出和标准错误都被重定向到了out.txt

输入输出符号-

把目录中所有内容复制到另一个目录的例子,其中-表示标准输入:

$ cat cpdir
#!/bin/bash
(cd $1 ; tar -cf - .) | (cd $2 ; tar -xvf -) 

bash cpdir dir1 dir2

减号-就是代表标准输出/标准输入,视命令而定。 -代替stdin和stdout的用法,stdin就是标准输入,stdout就是标准。

基础的shell脚本

调用脚本文件与参数

cat script.sh | bash /dev/stdin arguments

stdin更泛用的形式:

cat script.sh | bash -s - arguments

forkexec系统调用

  • 创建子shell:用户输入一第命令时,shell会用fork系统调用创建一个子shell。 这个子shell是当前系统的一个副本。
  • 尝试以二程序执行命令:子shell会假设命令调用的是一个可执行程序, 尝试以exec系统调用可执行程序。如果执行成功,可执行程序会代替子shell。
  • 尝试以shell脚本执行命令:如果exec系统调用失败,shell会认为命令不是可执行程序 而是一个shell脚本,子shell将执行脚本中的命令。

命令的分隔与分组

分隔命令:;和换行
$ cmd1 ; cmd2 ; cmd3
$ cmd1
$ cmd2
$ cmd3
命令在下一行继续:\'"

双引号中的换行作为字符串的一部分:

$ echo "Please enter the three values 
> required to complete the transaction."

Please enter the tree values required to complete the transcaction.

双引号中的反斜杠\转义紧接着的换行符被转义:

$ echo "Please enter the three values \
> required to complete the transaction."

Please enter the tree values required to complete the transcaction.

单引号不转义:

$ echo "Please enter the three values \
> required to complete the transaction."

Please enter the tree values \
required to complete the transcaction.
管道连接|与后台进程&

每个后台的作业都有不同的作业号:

$ a & b & c &
[1] 14290
[2] 14291
[3] 14292

管道连接的程序会当作一个单独的作业,Bash只显示一个进程号:

$ a | b | c &
[1] 14295
命令分组()

括号产生一个子shell,每个子shell作为一个作业,并拥有各自的运行环境, 不同的变量等配置:

$ (a ; b) & (c ; d) &

把目录中所有内容复制到另一个目录的例子,其中-表示标准输入:

$ cat cpdir
#!/bin/bash
(cd $1 ; tar -cf - .) | (cd $2 ; tar -xvf -) 

bash cpdir dir1 dir2

减号-就是代表标准输出/标准输入,视命令而定。 -代替stdin和stdout的用法,stdin就是标准输入,stdout就是标准。

任务控制

中断任务:Control + C

挂起任务:Control + Z

显示当前任务的作业号:jobs

可以通过百分号(%)加作业号的方式来指定作业。

指定任务8在后台执行

bg %8

指定任务8在前台执行

fg %8

终止任务:kill -TERM %8kill -15 %8

强制终止任务:kill -KILL %8kill -9 %8

操作目录栈

显示栈:dirs
$ dirs
~/literature
把目录压入栈中:pushed

通过参数把指定目录压入栈:

$ pushd ../demo             # ~/demo ~/literature
$ pwd                       # /home/sam/demo
$ pushd ../names            # ~/names ~/demo ~/literature
$ pwd                       # /home/sam/names

如果没有参数,就是把栈顶的两个目录交换:

$ pwd                        # /home/sam/names 
$ pushd                      # ~/demo ~/names ~/literature
$ pwd                        # /home/sam/demo

这样可以实现在两个目录之间来回切换的功能。

用数字代表栈里的第几个目录,栈顶目录为0。 比如-代表上一个目录,+2

$ dirs                       # ~/c ~/b ~/a
$ cd -                       # ~/ b
$ dirs                       # ~/b ~/c ~/a 
$ pushd +2                   # ~/a ~/c ~/b
把目录从栈中弹出:popd
$ dirs                       # ~/tmp/d ~/tmp/c ~/tmp/b ~/tmp/a
$ popd                       # ~/tmp/c ~/tmp/b ~/tmp/a
$ pwd                        # /home/adda/tmp/c
$ dirs                       # ~/tmp/c ~/tmp/b ~/tmp/a
$ popd +1                    # ~/tmp/c ~/tmp/a

参数和变量

用户创建变量

变量赋值:

$ myvar=abccc          # 变量赋值
$ echo $myvar          # abccc 

引用变量$

$ person=alex 
$ echo $person        # alex 

双引号字符串中的变量也会被替换:

$ echo "$person"      # alex 
$ echo '$person'      # $person 
$ echo \$person       # $person 

变量中的特殊字符,如*?也会被扩展。

花括号{}可以用来包起变量:

$ PREF=counter
$ WAY=$PREFclockwise
$ FAKE=$PREFfeit
$ echo $WAY $FAKE


$ PREF=counter
$ WAY=${PREF}clockwise
$ FAKE=${PREF}feit
$ echo $WAY $FAKE          # counterclockwise counterfeit

空白字符

$ person="alex and jenny"        # 
$ echo $person                   # alex and jenny
$ person=alex and jenny          # bash: and: 未找到命令
$ person="alex    and    jenny"  # 

多个空白字符会被压缩成一个:

$ echo $person                   # alex and jenny
$ echo "$person"                 # alex      and        jenny

变量赋值中的路径展开

在变量被替换时展开路径,双引号可以阻止路径展开:

$ ls                # alex.report  alex.summary
$ memo=alex* 
$ echo "$memo"      # alex*
$ echo  $memo       # alex.report alex.summary

删除变量:unset

$ echo $person              # alex and jenny

$ person=                   # 方法1:设为空
$ unset person              # 方法2:使用unset方法

变量的属性

变量不可变:readonly

$ person=jenny
$ readonly person
$ person=helen           # bash: person: readonly variable
$ unset person           # bash: unset: person: 无法取消设定: 只读 variable

不带参数时,readonly命令显示所有的只读变量列表:

$ readonly
declare -r BASHOPTS="cmdhist:complete_fullquote:expand_aliases:extquote:force_fignore:hostcomplete:interactive_comments:progcomp:promptvars:sourcepath"
declare -ir BASHPID
declare -ar BASH_VERSINFO=([0]="4" [1]="4" [2]="12" [3]="3" [4]="release" [5]="x86_64-unknown-cygwin")
declare -ir EUID="1155062"
declare -ir PPID="7692"
declare -r SHELLOPTS="braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor"
declare -ir UID="1155062"
declare -r person="jenny"

设置变量属性:declare和typeset

declare和typeset其实是同一个命令的不同名字, 通过操作符+-实现对属性的添加和删除操作。

注意:-是添加属性;+是删除属性。这点很坑。

常用属性:

  • -a:数组变量。
  • -f:函数名变量。
  • -i:整形变量。
  • -r:只读。也可以用readonly命令声明。
  • -x:全局变量。也可以用export命令声明。
添加属性
$ declare person1=alex
$ declare -r person2=jenny
$ declare -rx person3=helen
$ declare -x person

只读属性不能被删除。

删除属性

注意:-是添加属性;+是删除属性。这点很坑。

列出属性

declare不指定变量,就是列出指定属性的变量:

$ declare -r
declare -r BASHOPTS="cmdhist:complete_fullquote:expand_aliases:extquote:force_fignore:hostcomplete:interactive_comments:progcomp:promptvars:sourcepath"
declare -ir BASHPID
declare -ar BASH_VERSINFO=([0]="4" [1]="4" [2]="12" [3]="3" [4]="release" [5]="x86_64-unknown-cygwin")
declare -ir EUID="1155062"
declare -ir PPID="7692"
declare -r SHELLOPTS="braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor"
declare -ir UID="1155062"
declare -r person="jenny"
整数变量

变量一般是作为字符串存储,在涉及到数字操作时转为数字操作。 数字操作完成后,还是作为字符串存在变量里。

指定-i属性让变量一起作为整数属性存储:

$ typeset -i COUNT

关键字变量

HOME

PATH

MAIL

PS1

PS2

PS3

PS4

分隔字段(分词):IFS

IFS(Internal Field Separator,内部字段分隔符)是指定分隔参数的字符。

进程

进程结构

fork系统调用:进程的结构也是树形的。Linux系统的第一个进程是init进程, PID编号为1,是所有其他进程的祖先。

进程标识

PID编号:每个进程都有唯一的进程编号PID(Process identification)。

列出进程:ps

PPID指定创建了子进程的进程号:

$ sleep 30 &
[1] 10072

$ ps -f
     UID     PID    PPID  TTY        STIME COMMAND
  qwshan    7000    7008 pty1     10:39:53 /usr/bin/zsh
  qwshan   10072    7000 pty1     14:42:04 /usr/bin/sleep
  qwshan    9052    7000 pty1     14:42:05 /usr/bin/ps

列出进程树:pstree

$ pstree -p
init(1)-+-acpid(1395)
        |-atd(1758)
        |-crond(1702)
        ...

执行命令

fork和sleep

当shell中输入命令,shell创建一个子进程执行该命令,父进程进入休眠状态。 休眠状态不会占用CPU资源,等待被唤醒。

子进程完成后会把退出状态通知父进程任务执行是成功还是失败, 父进程(shell)被唤醒后等待用户输入下一个命令。

后台进程

命令后面加上$表示进程在后台执行。

这种情况下shell不会在创建子进程之后进入休眠状态。

shell内置命令

有些命令是shell内置的命令,这种情况下不会创建新的进程。

变量

一般情况下用户声明、初始化、修改的变量不会传递给子进程。

Bash下的内置命令export把变量声明为会被传递给子进程的全局变量。

命令历史机制

通过history命令可以查看输入的命令历史,时间顺序从早到晚排列:

$ history
116 ls
117 dir

命令历史相关变量

  • HISTSIZE:一个会话中保留的历史数量。
  • HISTFILE:历史文件,默认是~/.bash_history
  • HISTFILESIZE:会话之间保留的历史数量。

重新执行和编辑命令

fc

查看历史命令

查看历史命令

$ fc -l
10428  git difftool
10429  git commit -am 'expedia hotel sharding'
10430  git push
10431  git status
10432  ls
10433  cd ~/workspace/study

加上显示范围:

$ fc -l 10435 10438
10435  mvn clean install -DskipTests
10436  git status
10437  git difftool
10438  git status

按命令查找,从mvn开始到whereis结束的几条命令:

$ fc -l mvn git
10435  mvn clean install -DskipTests
10436  git status
10437  git difftool
10438  git status
10439  whereis aspell
编辑重新执行历史命令

fc可以重新编辑并执行之前执行过的命令,通过-e指定编辑命令时用的编辑器。

fc [-e editer] [first [last]]

通过修改FCEDIT变量可以指定默认的编辑器。

export FCEDIT=/usr/bin/emacs

例,编辑第21号命令:

$ fc -l
127 history | tail
128 echo '1'
129 echo '2'

$ fc -e vi 128
echo '177'
177

警告:退出编辑器时,无论是否保存,编辑器中的命令都会被执行。 所以退出时一定要保证编辑器中的文本是你想要执行的内容。 不想执行任何命令不能直接放弃保存退出,一定要清空所有文本才退出。

不调用编辑器直接重新执行历史命令
$ fc -s 1029
lpr letter.adams01

$ fc -s 1029
echo '001'

重新执行命令前可以替换文本:

$ fc -s adams=john 1029
lpr letter.john01

使用感叹号(!)引用事件

事件标志符
$ echo '001'
$ echo '002'
$ echo '003'
$ echo '004'
$ echo '005'

$ !!              # 马上执行上一条命令
echo '005'
'005'

$ !309            # 执行309号命令
echo '005'
005

$ !-8             # 执行前8条命令
echo '001'
001

$ !echo           # 最近一个以echo开头的命令
echo '001'
001

$ !?echo?         # 包含echo的命令
echo '005'
005
字符标志符
  1. n:第几个词,一般0表示命令的名称。
  2. ^:第一个(就是紧跟命令名称的)。
  3. $:最后的。
  4. m-n:范围,不指定m的话就是0-n
  5. n*:从n到最后。
  6. *:除了命令名以外的,相当于1*
  7. %:最近匹配?string?搜索的。
$ echo aa bb cc dd ee
aa bb cc dd ee

$ history
......
......
326  2017-05-08 15:17  echo aa bb cc dd ee ff gg

$ echo !326:2              # echo bb
$ echo !326:^              # echo aa
$ echo !326:^              # echo aa
$ echo !326:0 !326:$       # echo echo gg
$ echo !326:2-4            # echo bb cc dd
$ echo !326:0-$            # echo echo aa bb cc dd ee ff gg

例子,编辑上一个命令调用过的文件:

$ cat aa.txt
$ vim !$

当多个命令写在一个事件中时,参数对应的索引会有些变化:

$ !326 ; echo 111 222 333
echo aa bb cc dd ee ff gg ; echo 111 222 333
aa bb cc dd ee ff gg
111 222 333

$ history
......
  334  echo aa bb cc dd ee ff gg ; echo 111 222 333

$ echo !334:10
echo 111
111

$ echo !334:6-10
echo ff gg ; echo 111
ff gg
111
修饰符

替换命令中的文本:

[g]s/old/new/

快速替换是一种简写,直接作用于上一条命令:

^old^new^
#相当于
!!:s/old/new/

最后一个^后面直接跟RETURN,那么最后一个^是可以省略的,

例:

$ car aaa.txt bbb.txt
bash: car: 未找到命令

$ !!:s/car/cat/
cat aaa.txt bbb.txt
aaaaa
bbbbb

$ ^car^cat
cat aaa.txt bbb.txt
aaaaa
bbbbb

其他修饰符:

  • p(print-not):只打印出命令,不要执行。
  • e(extension):只取扩展名。
  • r(root):删除文件扩展名。
  • h(head):去掉路径名的最后一部分。
  • t(tail):只保留路径最后一部分。
  • q(quote):引用该替换,防止进一步的替换操作。
  • x:与q相似,除了单独引用替换中的每个字。

例:

$ ls /home/qwshan/tmp/aaa.txt
/home/qwshan/tmp/aaa.txt

$ !!:p                              # 只打印出命令,不执行
ls /home/qwshan/tmp/aaa.txt  

$ !!:h:p                            # 去掉路径最后,打印不执行
ls /home/qwshan/tmp

别名

一般定义在~/.bashrc中,语法:

alias                        # 查看所有的别名
alias name                   # 查看指定别名的值
alias name=value             # 定义别名
unalias                      # 删除别名
  • 等号两边不能有空格;有白字符要用引号包起来。
  • 别名不能替换自己,避免无限递归。
  • 在非交互式的Shell中(如脚本),别名是禁止的。
alias ls='ls -F'          # 无效,防止无限递归替换自己。

别名中的单引号与双引号

双引号会扩展字符串中的变量,单引号不会:

$ alias dirA="echo Working directory is $PWD"
$ alias dirB='echo Working directory is $PWD'

$ alias dirA
alias dirA='echo Working directory is /home/qwshan/tmp'

$ alias dirB
alias dirB='echo Working directory is $PWD'

防止别名替换

在命令前加反斜杠可以避免别名替换:

$ \ls
aa.txt bb.cab cc.txt

函数

格式:

[function] function-name ()
{
	commands
}

调用函数的实参在函数中用$n(n从1开始)。

$ function argi() { echo "$1" }

$ argi aa bb cc
aa

可以把函数定义加载到bash的初始化脚本中(如:~/.bash_profile)。

.(dot命令)命令让配置脚本立即生效:

$ . ~/.bash_profile

操作交互式命令行

历史扩展

关闭历史扩展:

set +o histexpand

别名替换

关闭别名替换:

shopt -u expand_aliases

解析和扫描命令行

在经过了历史替换和别名替换之后 ,Shell还会处理特殊字符和模式。

命令行扩展

Bash扫描输入的命令,并按以下顺序进行扩展:

  1. 花括号扩展
  2. 代字符扩展
  3. 参数扩展和变量扩展
  4. 算术扩展
  5. 命令替换
  6. 分词
  7. 路径名扩展
  8. 处理替换

引用删除:在做完了所有的替换工作以后,Bash会去掉那些不是扩展结果产生的引号, 斜杠等字符。

双引号会抑制参数扩展和变量扩展外所有的扩展方式;单引号会抑制所有的扩展方式。

花括号扩展

$ echo chap_{one,tow,three}.txt

$ mkdir chap_{A,B,C,D,E}

$ mdkir vrs[A-E]

代字符扩展

$ echo $HOME           # /home/qwshan 
$ echo ~               # /home/qwshan 
$ echo ~/letter        # /home/qwshan/letter 
$ echo ~letter         # ~letter 

参数扩展

$开头,后面不跟括号的是变量。

$ x=23 y=37
$ cor=$(wc -l aa.txt | cut -c 1-2)
$ echo $cor                           # 78

算术扩展

$((expression))

不要放到单引号里,不然变量不扩展了:

$ echo There are $((60*60*24*365)) seconds in a non-leap year.
There are 31536000 seconds in a non-leap year.

$ echo "There are $((60*60*24*365)) seconds in a non-leap year."
There are 31536000 seconds in a non-leap year.

$ echo 'There are $((60*60*24*365)) seconds in a non-leap year.'
There are $((60*60*24*365)) seconds in a non-leap year.

单个变量前的$是可以省略的:

$ x=23 y=37

$ echo $((2*$x + 3*$y))      # 157 
$ echo $((2*x + 3*y))        # 157
$ wc -l aa.txt                                         # 78 aa.txt 
$ wc -l aa.txt | cut -c 1-2                            # 78
$ echo $(( $(wc -l aa.txt | cut -c 1-2)/2 + 1))        # 40

内置命令let,可以用来计算表达式的值:

$ let "numpages=$(wc -l < letter.txt)/66 +1"

let也可以给多个变量赋值:

$ let a=S+3 b=7+2          # 8 9

\((())\)()

$(())是一个算术表达式,而是不是命令替换。如果要插入一个子shell, 在括号中加一个空格:$( () )

分词

在命令行处理扩展与替换时,是以IFS定义的分隔符来分隔一个一个词的。

命令替换

命令替换就是把command所执行的结果再作为命令执行。格式:

$(command)

或者:

`command`

例:

$ echo "You are in $(pwd) directory"
You are in /home/qwshan directory

路径名扩展

*?[]会被bash扩展路径,除非设置了noglob标记。

$ ls letter*        # letter1  letter2  letter3 
$ var=letter*       # 
$ set | grep var    # var='letter*' 
$ echo '$var'       # $var 
$ echo "$var"       # letter* 
$ echo $var         # letter1 letter2 letter3

进程替换

Bash还可以用进程替换文件名参数,语法:

<(command)

command的结果写入命名管道(FIFO)。Shell把管道的替换掉这个参数。 如果在处理期间,的去这个参数用于某个输入文件的名称,那么读取的是command的输出。

类似还可以用>(command)替换掉标准输入。

例:

$ sort -m -f <(grep "(^A-Z)..$" memol | sort) < (grep ".*aba.*" memo2 | sort)

在两个输入文件都已经排序的情况下,sort -m把两个列表合并为一个表。 而两个列表中的每个单词列表都在管道提取出某个模式的单词产生的。