REPL解析过程
当你在命令行中输入字符时,比如最简单的,
$ ls
bash会查找ls然后执行
但是,如果你输入任何允许的东西,bash怎么处理,比如:
$ cmd=ls
$ $cmd
bash会如何解释$cmd
呢?这里,变量替换是生效的,因此bash执行ls。
如果cmd中包含多个参数呢?
$ cmd="ls -l"
$ $cmd
bash会使用IFS来分割$cmd,因为它没有使用双引号包含,所以变量会展开之后分割,考虑:
$ cmd="ls,-l"
$ IFS=,
$ $cmd
如果你unset IFS,则$cmd会报错找不到"ls,-l"。
除了变量替换之外,命令的解析支持$’\x10’语法吗?答案是支持。
# 等价于echo yes
$ $'\x65ch\x6f' yes
yes
eval
eval会将目标参数使用空格join成字符串,然后执行
IFS
当bash遇到一个字符串序列时,如果它看到这个字符串没有使用引号,就会通过IFS将这个字符串展开成多个域。IFS影响的是字符串的展开,而不会影响脚本的命令按空格分隔的原则。
注意,如下的命令不会生效
a='echo,-n,yes'
IFS=, $a
并不会执行echo -n yes
, 因为$a的解析是在IFS=,这一行语句之前,IFS只是影响导出的环境变量。
交互
特殊字符
Bash core
bultin-in
Subshell和子进程的区别
subshell - tldp
当使用()运行子命令时,子命令是在subshell中执行的,subshell的赋值不会影响父shell,但父shell的未导出的变量,函数,数组等在subshell中可读。
$$ 在子shell和父shell中都是一致的
$BASHPID 则是每个shell自有的
C-Style
在bash中某些地方可以使用c语言风格的语法
{a,b}展开
{xxx,yyy,zzz,…}其中{}是展开符号,如
echo {a,b}{c,d}
--> ac ad bc bd
{} 匿名函数
{}创建一个匿名函数,但是不能使用local
[], [[]],和test
注意,[和test是同义词,都是shell的内置命令,但是[[是关键字。
$[ … ]
正数展开,和$((…))一样
&>
将stdout和stderr都重置
(echo yes;echo no >&2) &>/dev/null
退出值
let和(())的退出值是根据结果是否是0来判断的, 为0时返回1(错误),非0返回0(正确)。
可用于一般的条件表达式中
for(…;…;…)
for((i=0;i<10;i++));do
echo $i
done
condition?a:b
: $((b=a<10?1:0))
注意, :在这里只是为了避免赋值的副作用
含有\x00的字符串
将含有\x00的字符串赋值给变量时,会产生截断:
a=$'aa\x00adf'
"$a"
--> bash aa: command not found
echo "${#a}"
-->2
如上所见,字符串只被识别到2个字符。
如果需要传递这样的字符串,可以通过管道进行操作。
echo -ne 'ab\x00c'|wc -c
--> 4
字符串
参数替换(Parameter Substitution)
变量形式 | 变量未声明 | 变量已声明,值为null | 变量不为null | 说明 |
---|---|---|---|---|
${a-default} | default | $a | $a | 使用默认值:变量只需声明即可避免使用默认值 |
${a:-default} | default | default | $a | 使用默认值:变量需声明且值非空才能避免使用默认值 |
${a=default} | a=default | a不变 | a不变 | 设置默认值:变量只需声明即可避免使用默认值 |
${a:=default} | a=default | a=default | a不变 | 设置默认值:变量需声明,且值非空才能避免设置默认值 |
${a+alt_value} | null | alt_value | alt_value | 使用可选值:变量需声明 |
${a:+alt_value} | null | null | alt_value | 使用可选值:变量需声明,且值非空 |
${a?err} | 错误 | 正确 | 正确 | 变量断言:变量需声明 |
${a:?err} | 错误 | 错误 | 正确 | 变量断言:变量需声明,且值非空 |
注:上面的每种形式都有一个:
的版本,:
的目的是区分变量为null的两种情况中,未声明和已声明但是为null的情况。 基本上,不带:
的版本,只要已声明,无论值是null或非null,结果都是一致的。而带:
的版本,未声明和已声明但值null是一致的。
总的来说, 不带:
的版本,只关心是否声明;而带:
的版本,只关心值是否为null.
字符串
LDP-manipulate strings
${a:from:len}
子串,也可用于数组
${}替换:规则: #表示开头,%表示结尾, /表示替换
/# 表示匹配开头并替换, /%表示匹配末尾并替换
/表示替换所有
expr
Here document
基本形式,进行变量替换
user:dar export NAME=test
user:dar cat << EOF
> name=$NAME
> EOF
name=test
带引号(单引号和双引号一样),不进行变量替换
user:dar export NAME=test
user:dar cat << "EOF"
> name=$NAME
> EOF
name=$NAME
无需对变量进行双引号引用(不会进行expand)
字符串作为输入
当使用变量作为内联字符串输入时,无需对变量进行双引号引用
user:dar a="a b c
> d e
> "
user:dar cat -e <<< $a
a b c$
d e$
$
词法环境
词法环境,也就是lex environment, 是指在
${s/A/B}, 其中A和B都是词法环境,A和B会作为一个单独的词法环境进行解析,而不受外围的语法影响。
此外,$(S)中S也创造了一个词法环境
在=~ 的右侧,也创建了一个词法环境,但是注意,在pattern中可以使用 $’\n’来表示特殊字符
$ [[ ab$'\n'c =~ [$'\n'] ]];echo $?
0
=~
关于Pattern,参考Regular Expression Posix Extended
[[ ]]
在 [[]]
无需对变量进行双引号引用,它们不会像 [
那样([
实际上是命令,而 [[
是bash内建的操作符)
user:dar a="a b c"
user:dar [[ -n $a ]];echo $?
0
user:dar [ -n $a ]];echo $?
-bash: [: missing `]'
2
变量赋值(包括拼接,不会进行expand)
$ a="ab cd"
$ b=" ef z"
$ c=$a$b
$ echo "$c"
ab cd ef z
字符串替换
只有${…/…/…}在语法层面上符合 / … /的格式即可,即使变量中含有/分隔符,也不会进行expand
a=' b c/ d '
b=' x f/ c '
c=X.$a$b.X
echo "$c" # X. b c/ d x f/ c .X
echo ${c/$a$b/Z} # X.Z.X
需要对字符串变量进行双引号引用
文件重定向
当包含文件名的变量含有分隔符号(空白符,换行等)时,在重定向中必须使用双引号引用
user:dar echo yes > "a b.txt"
user:dar f="a b.txt"
user:dar cat < "$f"
yes
user:dar cat < $f
-bash: $f: ambiguous redirect
Code Snippets
for each line
使用read读取文件的每一行,使用’'进行分割
IFS=$'\n'
read -d '' -a arr <<< $data
解释:
IFS=$’\n’用于设置域分割符合是换行符,这样read数组的时候就是会使用换行符作为分隔
-d ‘’ 用于设置delimeter,用于指示read读取所有的输入
-a arr 赋值到数组中
<<< $data 字符串输入重定向
Example Usage
lsof查找
ps显示进程
gitm.sh
显示当前git仓库消息
#!/usr/bin/env bash
git log --format=%s -n1 "$@"
kill-port.sh
杀掉端口对应的进程
#!/usr/bin/env bash
if [[ -z $1 ]];then
echo "requires port" >&2
exit 1
fi
pid=$(lsof -i ":$1"|tail -n1|awk '{print $2}')
if [[ -z $pid ]];then
echo "port not open:$1" >&2
exit 1
fi
echo "killing pid=$pid for port=$1" >&2
kill -9 "$pid"
显示git分支
# Git branch in prompt.
parse_git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ (\1)/'
}
PS1="\u@\h \W\[\033[32m\]\$(parse_git_branch)\[\033[00m\] $ "
PS1定制
参考:http://ezprompt.net/
\A = 11:38
\u = 用户名
\h = hostname
PS1="\u@\h \w\[\033[32m\]\$(parse_git_branch)\[\033[00m\] \A "
效果:
nick@Desktop ~/project (develop) 11:41
如果目录显示过于长,可以将
换行
PS1="\u@\h \w\[\033[32m\]\$(parse_git_branch)\[\033[00m\] \A \n "
特殊图标:
unicode查询
苹果:echo -ne '\xef\xa3\xbf'
Linux企鹅:🐧 echo -ne '\xf0\x9f\x90\xa7'
目录缩写
shortpath(){
echo ${PWD/#'/home/dar/wpath'/'$W'};
}
PS1='\u@\h \e[33m$(shortpath)\[\033[32m\]$(parse_git_branch)\[\033[00m\] \A '
IP
用于区分
ipaddr(){
addr=$(ip addr show eth0 |awk '/inet /{print $2}'|cut -d '/' -f1)
echo -n "$addr"
}
# use PS1='\u@$(ipaddr)'
忽略不可打印的字符
在定制PS1时,应当确保指示bash忽略带颜色的字符,不然就可能导致输入提前换行。
http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/nonprintingchars.html
汇总
Linux PS1配置:
parse_git_branch() {
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/ (\1)/'
}
shortpath(){
p=${PWD/#'/Users/bytedance'/'~'}
p=${p/#'/home/xiaohuadong'/'~'}
p=${p/#'~/Projects/webcast'/'$GOPATH'}
p=${p/#'$GOPATH/src/code.byted.org/webcast'/'$W'}
echo -n "$p"
}
ipaddr(){
addr=$(ip addr show eth0 |awk '/inet /{print $2}'|cut -d '/' -f1)
echo -n "$addr"
}
# the linux penguin icon
PENGUIN=$(echo -ne '\xf0\x9f\x90\xa7')
#
APPLE=$(echo -ne '\xef\xa3\xbf')
# PS1='\u@\h:\w \A\n\$ '
PS1='\u@$(ipaddr) \e[33m$(shortpath)\[\033[32m\]$(parse_git_branch)\[\033[00m\] \A \n$PENGUIN '
Shell编写指南
几大原则
关于 /bin, /sbin, /usr/bin, /usr/local/bin, 你需要了解 /usr/bin是系统层级的, /usr/local/bin是organization层级的,因此,对于env这样的工具,应当指定 /usr/bin/env
此外,对于要不要将子工具暴露到路径中,这是一个关于如何组织代码的问题,你当然需要避免将子工具暴露到路径中。这样一来,你就只能依赖某些特殊的项目