Skip to content

关于$的理解

对于Shell的一点理解

$会引用变量的值

echo的使用

-n 取消换行 -e 开启引号(单引 双引)中的反斜杠转义( backslash escapes ) -E 禁止反斜杠转义

如果命令较长 可以用加上反斜杠写成多行 echo hello \world

sh
$ 不会在单引号中进行转义
\ 不会在单引号中进行转义 在没有引号的情况 奇数会进行换行转义 偶数直接转义
  在有双引号的情况下会转义可能会转义反斜杠(反斜杠后面紧跟着双引号的时候)
* 在不加引号的情况下可以进行文件扩展

命令转换(command subsititution

`...`(backticks | back quotes) 和 $(...) 都是命令置换====> 把命令的执行结果返回。

`...`在使用时尤其要注意思 美元符号 反斜杠 反引号 的使用,建议优先使用 $(...) 参考链接

sh
# `...` 里如果有转义符(反斜杠 backslashes)时可能会被以不明显的方式处理掉,$(...) 不会
echo "`echo \\a`" "$(echo \\a)" # a \a
echo "`echo \\\\a`" "$(echo \\\\a)" #  \a \\a
# 下面的例子,使用的 *single quotes* 也是一样的
foo=`echo '\\'`;bar=$(echo '\\')
echo "foo is $foo, bar is $bar" # foo is \, bar is \\

# 不明显的方式表现为 有多对的反斜杠时会向上取整 (Math.ceil(n/2))
foo="`echo '\aa'`"; bar="$(echo '\\aa')" # \aa \\aa
foo="`echo '\\\\aa'`";bar="$(echo '\\aa')" # \\a \\aa
foo="`echo \"\\\\aa\"`";bar="$(echo "\\aa")" # \aa  \aa
foo="`echo '\\\\\aa'`"; bar="$(echo '\\\\\aaaa')" # \\\aa \\\\\aaaa

# # $(...) 中的引号是一对的,而不是 x 前面的引号 与 $y 前面的引号是一对
# echo "x is $(sed ... <<< "$y")"
# echo "x is `sed ... <<< \"$y\"`"

# # 对于嵌套的话, $(...)更好用
# x=$(grep -F "$(dirname "$path")" file)
# x=`grep -F "\`dirname \"$path\"\`" file`

$(cat file) 等同于 $(< file)

eval的使用

sh
# eval 可以用来回显简单变量
foo=bar
echo $foo
eval echo $foo

# eval 执行字符串命令
bar="ls -AFl"
echo $bar
eval $bar

# eval 可以显示出传递给脚本的最后一个参数
echo "传进来$#个参数"
echo "当前进程ID为$$"
echo '---------------'
echo \$$# # $3字符串
eval echo \$$# # 字符串 asdlfasd
# echo eval echo \$$# # eval echo $1
echo "当前最后一个参数是$(eval echo \$$#)" # 不明白 $(字符串) 为什么还是字符串

echo '---------------'
# 实现文件的第一列成为变量名 第二列成为变量值
while read NAME VALUE
do
eval "${NAME}=${VALUE}"
done <a.txt
echo "$COMMANY $LANGUE $LIKE"

算术表达式

sh
# ((...)) 语法可以进行整数的算术运算,如果读取运算结果需要在前加上$ ,使其成为算术表达式,返回算术运算的值
# 支持 + - * / % **(指数) ++ --  需要注意的是 除法运算的返回结果总是整数
echo $((3+3))
echo $? #  算术结果不为0,都算成功

((foo= 5 +  5))
echo $foo

((5 - 5))
echo $? # 失败  环境变量 $? 为1

echo $(((5**2)*10)) # 支持嵌套
echo $(( $((5**2)) * 10)) # 与上面是相同的

# echo $((2.5 + 3)) # 只能进行整数运算

num=3
echo $((num + 1)) # echo $(($num + 1)) 变量名加上$ 也是可行的

echo $(("foo" + 1))
echo $(("test" + 1)) # 字符串(双引号)会认为是变量名,如不存在,会将其作为空值,$((...)) 会将空值当作0,因此不会报错
# echo $((‘test’ + 1)) #  单引号报错 ‘test’ + 1: syntax error: operand expected (error token is "‘test’ + 1")

foo=hello
hello=3
echo $((foo + 3)) # 变量foo的值是hello,而hello也会被看作变量名,因此可以写出动态替换的代码

echo $[3+30] # $[...]是以前的写法,不建议使用

疑惑

sh
#!/bin/sh

str="hello world"
# 读取变量的语法 $str 可以看是 ${str} 的简写形式
echo $str
echo ${str}

a="hello";
b=a;
echo ${!b} # 输出结果为 hello
echo ${a}_file # 输出结果为 hello_file

变量

  1. 特殊变量
sh
$? # 上一条命令的执行结果 0 表示成功 !0 表示失败
$_ # 上一个命令(可能是多条命令的组合)的最后个参数 是最后一个参数 !$是指上一行命令的最后一个参数 !* 上一行命令的所有参数

mkdir a && cd $_ # $_ 代表 a
echo aaa bbb ccc

$$ # 当前 shell的进程ID(PID)  可通过 ps 命令查看
$! # 后台异步命令的进程ID

$0 # 当前 shell 脚本的名称

$- # 当前脚本的启动参数
$# # 脚本的参数数量
$@ # 脚本的参数值

foo='hello world' # varname 必须为变量名
${#varname}
echo ${#foo} # 返回length 11
${varname:offset:lenth} # offset 为负数时必须要有空格 与模式匹配区分
echo ${#foo} # 11
echo ${foo:6} # world
echo ${foo:6:3} # wor
echo ${foo: -5:3} # wor
echo ${foo: -5} # world
echo ${foo: -5:-1} # worl
echo ${foo:6:-3} #wo

# 大小写转换
a=abcdefg
b=${a^^}
c=${b,,}
echo $a
echo $b; # 大写
echo $c; # 小写
  1. 默认值
sh
echo ${foo:-bar} # 如果foo存在,则返回它的值,反之则返回bar
echo $foo

echo ${bar:=baz} # 如果bar存在,则返回它的值,反之将bar的值设为baz并返回
echo $bar

echo ${baz:+qux} # 如果baz存在,则返回qux,反之返回空
echo $baz

echo ${qux:?不存在} # 如果qux 存在,则返回它的值,否则打印出 qux: 不存在 并中断脚本的执行
echo ${1:?参数不存在} # 当我们执行脚本不传参数时会报错,并且后续代码不会在执行

foo=USERNAME
echo ${!foo} # 展开值最终为 $USERNAME 的值

echo ${!S*} # 扩展为以S前缀开关的变量名称,可以循环 等同于 echo ${!S@}
  1. 搜索与替换
sh
# 头部模式匹配 ${varname#pattern} 非贪婪匹配 ${varname##pattern} 贪婪匹配
# 头部匹配 不会改变原变量  匹配模式支持 * ? [] 等通配符 匹配失败返回原字符串
bar='abcabcdefgabcdefg'
echo ${bar#abc} #abcefgabcefg
echo ${bar##abc} #abcdefgabcdefg
echo ${bar#*defg} #abcdefg
echo ${bar#sdsdas} # 匹配失败,返回原字符串 abcabcdefgabcdefg
echo ${bar##*defg} # 空
echo $bar # 不会改变原变量

path=/users/holger/downloads/long.file.name
echo ${path#/*/} # holger/downloads/long.file.name 非贪婪匹配
echo ${path##/*/} #long.file.name 贪婪匹配
echo ${path#*/} # holger/downloads/long.file.name
echo ${path##*/} # logn.file.name 直接取文件名

echo ${bar/#abc/123} #123abcdefgabcdefg
echo ${bar/##abc/456}

echo '================='
# 尾部模式匹配 ${varname%pattern} 非贪婪匹配 ${varname%%pattern} 贪婪匹配

echo ${path%.*}  # /users/holger/downloads/long.file
echo ${path%%.*} # /users/holger/downloads/long
echo ${path%/*} # /users/holger/downloads 返回目录
file=avatar.png
echo ${file%.*}.jpg # 改文件后缀名(suffix)

echo ${file/%png/jpg} # 尾部替换

#任意位置模式匹配 ${varname/pattern} 非贪婪匹配 ${varname//pattern} 贪婪匹配
path=/home/foo/foo/foo.name
echo ${path/foo/bar} # /home/bar/foo/foo.name
echo ${path//foo/bar} # /home/bar/bar/bar.name
echo ${path/.*/} # /home/foo/foo/foo 删除后缀
echo '============='
echo -e ${PATH//:/'\n'} # 让 PATH 变量进行换行,提高可读性

扩展模式

sh
 touch a.txt b.txt ab.txt 'a b'.txt

# ? 匹配单个字符 如果匹配不到,则原样输出
echo ?.txt # a.txt b.txt ?(不要与正则语法 题词语法(extglob)混为一谈)
echo ??.txt # ab.txt
echo ???.txt # a b.txt 网道教程说不匹配空格 其实是可以匹配的

echo *.txt # a b.txt a.txt ab.txt b.txt *(匹配任意数量的字符,包含0 区间{0,n} )  不会匹配子目录
echo .* # . .. .gitignore 三个文件
echo .[!.]* # .gitignore 等同于 .[!.]*
echo */*.txt # dir/a.txt 只匹配一层子目录
echo **/*.txt # 可以匹配顶层与任意深度的文件 globstar(默认打开状态),如未打开,执行shopt -s globstar

echo [ab].txt # a.txt b.txt 不包括 ab.txt
ls [ab].txt # 如果 a b 已删除,会直接 ls [ab].txt 找不到,则会报错  ls: cannot access '[ab].txt': No such file or directory

ls [!a].txt # b.txt 等同于 [^a].txt 如果找不到,则找单个字符的,还找不到时的话,则 ls [!a.txt]
rm -rf * .[!.]* # 可以删除当前目录所有文件
ls [a-c].txt # 可以匹配 a.txt b.txt c.txt 如果要包含 - 的文件 则需要放在中括号开关或者结尾 [-aeiou] [aeiou-] 匹配 [ 文件 [[aeiou]

echo {1,2,3} # 1 2 3
echo d{a,e,i,o,u}g # dag deg dig dog dug   中间不能有空格 echo d{a,e,i,o, u}g d{a,e,i,o, u}g
echo a.log{,.bak} # a.log a.log.bak 逗号前面可以没有值,表示为空
echo {j{p,pe}g,png} # jpg jpeg png 大括号可以嵌套
echo /bin/{cat,b*} # /bin/cat /bin/b2sum.exe /bin/backup... 大括号可以与其他模式联用,优先于其他模式 等同于 echo /bin/cat;echo /bin/b*

echo {a..e} # a b c d e
echo {d..a} # d c b a

# touch [a..d].txt 与 touch {a..d}.txt的区别,[] 创建了[a-z].txt文件,{a..d} 创建了a b c d 四个文件
# echo 也是相同情况 {} 大括号不是文件名扩展,而是扩展值
  • shopt
sh
shopt -s optname # 打开某个参数 set
shopt -u optname # 关闭某个参数 unset
shopt optname # 查询当前参数开关状态
shopt # 查看所有 optname 状态

正常情况下 ls * 是不会显示隐藏文件 dotglob 打开后 可以显示 .开关的隐藏文件

假设没有以b开关的文件(名|夹),正常情况下 rm b* ,会报错 rm: cannot remove 'b*': No such file or directory
当打开 failglob 后,报错 bash: no match: b* 因为 b* 不匹配任何文件,bash 直接报错,rm 命令不再执行
当打开 nullglob 后,报错 rm: missing operand 因为 b* 扩展成了空字符串 ,rm 缺少参数

当打开 nocaseglob 后, ls B*;如果有小写b开头的文件,也可以显示出来

文件结构 a.txt ab.txt abc.txt abcbc.txt    extglob(默认打开)
echo a?(bc).txt # 匹配{0,1}次 a.txt abc.txt
echo a*(bc).txt # 匹配{0,n}次 a.txt abc.txt abcbc.txt
echo a+(bc).txt # 匹配{1,n}次 abc.txt abcbc.txt
echo a@(bc).txt # 匹配1次 abc.txt
echo a!(bc).txt # 除了bc a.txt ab.txt abcbc.txt

文件结构 a.txt  sub1/b.txt   sub1/sub2/c.txt
ls *.txt */*.txt */*/*.txt # 未打开 globstar 时,查找全部以txt结尾的文件
ls **/*.txt # 打开后 a.txt  sub1/b.txt  sub1/sub2/c.txt

Shell 输入输出重定向

重定向

sh
# 文件描述符 stdin(0) stdout(1) stderr(2)

echo "hello world" 1> a.txt # 标准输出重定向到时a.txt 1表示输出后面不能有空格
echo "hello world" > a.txt # 1 可省略
echo "hello world" >> a.txt # >> 表示追加

(echo "hello world" && llll) 2> /dev/null # 将标准错误重定向到 /dev/null(黑洞)
lsss 2>> a.txt # 标准错误重定向到 a.txt
(echo "hello world" && lsss) 1>> a.txt 2>> a.txt # 标准输出 标准错误 都重定向到 a.txt
(echo "hello world" && lsss) 1>> a.txt 2>&1 #简写语法 (2>&1)是追加的意思
(echo "hello world" && lsss) &>> a.txt # 进一步简写 如果写成 (&> a.txt)没有追加
(echo "hello world" && lsss) 2>&1 >/dev/null # 把标准错误输出到终端, 标准输出重定向到 /dev/null

# 输入重定向
cat < a.txt > b.txt # 将 a 文本内容输入到b中 等同于 cat a.txt > b.txt
cat < a.txt >> b.txt # 将 a 文本内容追加到 b 文本中
# 对于中文乱码的情况
ipconfig //displaydns | iconv -f GB2312 -t UTF-8 > dnsCache.txt

here document

sh
foo=USERNAME

# 默认会发生转义
cat << EOF
AAA
$foo
'$foo'
"$foo"
EOF

# 如不想转义可以把 here 文档的开始标记放入引号(单双都可以)中
cat << 'EOF'
AAA
$foo
'$foo'
"$foo"
EOF

# 如果想对齐文档格式 需要把缩进(indent)调为制表符 << 后面加上 -
cat <<- 'EOF'
	AAA
	$foo
	'$foo'
	"$foo"
EOF

# hero document 本质是重定向,相当于 echo 命令的重定向。所以 here doc 对于 echo 命令无效。上面的相当于 echo string | command

# 4 将会定稿 here.txt
wc -l << EOF >here.txt
aaa
bbb
ccc
EOF

# 将输入文本写入文件
cat << EOF >a.txt
aaa
bbb
EOF

here string

sh
# here string 是 here document 的变体,
cat <<< 'hello world'  # 控制台输出传给 cat,等同于  echo hello world | cat

# a 文件写入 hello world
echo hello world | cat > a.txt
echo -e "hello\nworld" | cat | wc -l # 终端输出2

# < 改变读取(stdin) > 改变输出(stdout)

常用命令

  • rm
sh
# -r 递归 -f 强制 --force
  • ls
sh
# a 显示所有文件(包含隐藏文件 .开头的) A 显示所有文件(除了 . .. )
# F显示文件类型(文件夹显示/ 链接文件显示@ 可执行文件显示*) R 显示文件(若子目录有文件也依次显示)
# l 显示长信息 h 显示可读性的文件大小 i ==> inode
sh
# -z gzip -c 创建 -x 解压 -v verbose显示处理过程 -f 备份文件 -t 显示归档文件的内容(不会解压缩)
# --remove-files 压缩文件的同时删除源文件(复数说明不能用于 删除归档) -C 解压后放入指定目录
# 第一位置永远放 tar.gz 文件,不管是归档还是压缩
  • ln

warn: windows 下 软链接(symbolic links)可能会失效,变成复制(inode 不同) 参考

硬链接(hard links)表现正常 ,硬链接与复制不同

sh
# ln target linkName 硬链接 只能创建文件
# ln -s targe linkName 软链接(符号链接) 文件 || 目录 对于文件目录来说 rm linkName 不要加上/ 否则会删除target 文件

ln a1.txt a2.txt # 可通过 ls -il 查看 inode 是相同的,修改a1 a2 也会被修改

# 软链接(文件) rm aFileSoftLink 不影响 target
ln -s test/a/a.txt  ./aFileSoftLink
# 软链接(目录) rm aDirSoftLink ===> 无影响 (与win 右键删除一样)
# rm aDirSoftLink/ 若目录后加上斜杠,当前软链接目录还在文件则被删除(影响 target)
ln -s ./test/a aDirSoftLink

# windows 使用 cmd 创建软链接
mklink /j "D:\a" "C:\Users\HJ\Desktop\link"  # D盘a文件夹指向 link 文件夹

vim

  • 插入模式

i 当前光标位置插入 I 当前行行首插入

a 当前光标下一个位置插入 A 当前行行尾插入

o 当前行的上一行插入 O 当前行的下一行插入

s 删除当前光标下一个位置的内容,并插入 S 清除当前行内容并在行首插入

Curl 下载

Usage:

  • -L 请求重定向
  • -x --proxy 使用代理
  • -O --remote-name URL 最后一部分当作文件名
  • -J --remote-header-name 服务器可能会提供一个名为 Content-Disposition 作为响应,这个响应包含传输内容的文件名建议,优先及比 -O 要高
  • -o --output 可以是文件名及路径拼接文件名 -o- | -o - 可能强制二进制文件输出到终端, stdout > 可以重定向
  • -C --continue-at 下载续传。 不能与 -J 属性组合
  • --create-dirs 用于创建下载目录
  • --output-dir 下载目录,如果目录不存在,不会自动创建 可以使用 --create-dirs
sh
curl -LOx http://localhost:10809 https://github.com/2dust/v2rayN/releases/download/5.38/v2rayN-Core.zip
curl -LOx http://localhost:10809 https://github.com/2dust/v2rayN/releases/download/5.38/v2rayN-Core.zip -C -
curl -Lx http://localhost:10809 https://github.com/2dust/v2rayN/releases/download/5.38/v2rayN-Core.zip >./v2rayN-Core.zip
curl -Ox http://localhost:10809 https://raw.githubusercontent.com/Kimentanm/aptv/master/m3u/iptv.m3u
sh
curl -sx http://localshot:10809 https://raw.githubusercontent.com/holger2138/holger2138.github.io/main/docs/test.sh | sh # 执行远程脚本
# <( 不能有空格 有参数时
sh <(curl -sx $proxy https://raw.githubusercontent.com/holger2138/holger2138.github.io/main/docs/test.sh) 123
# bash -s  -- 将 bash 后面的内容视为参数而不是选项
curl -sx $proxy https://raw.githubusercontent.com/holger2138/holger2138.github.io/main/docs/test.sh | sh -s 123
curl -sx $proxy https://raw.githubusercontent.com/holger2138/holger2138.github.io/main/docs/test.sh | sh -s -- 123
curl -sx $proxy https://raw.githubusercontent.com/holger2138/holger2138.github.io/main/docs/test.sh | sh /dev/stdin 123

scp

sh
# linux 文件传入 Windows
# 普通 windows 用户可能需要开启 openssh 服务 
# 终端管理员执行 net start sshd
scp file username@ip:/c:/

# 下载 linux 文件到时本地
# 对于 openwrt 可能会提示以下错误
# sh: /opt/libexec/sftp-server: not found scp: Connection closed 需要加 -O
scp admin@192.168.1.50:/tmp/home/root/dump.txt ./Desktop/test.txt

Released under the MIT License | Copyright © 2022-present HOUJIAN