Linux Shell基本语法入门
?LinuxUser001
- 首先,我不得不回避一个问题。使用Linux作为主力系统的或者正在管理Linux服务器的同学们,你们真的熟悉bash和命令行吗?
- 要想看这篇文章,懂一点基本命令是必不可少的。当然,过来人的lu001还是会关照那些从来没用过Linux的同学。
##bash是最重要的
- 这篇文章基于bash的语法而写(你们只需要知道bash - Borune-Again Shell是一个从sh - Borune Shell移植过来的命令行解释器即可,其他请cn.bing.com),所以请使用zsh的同学谨慎食用。
命令行第一式:基本命令
- 其实基本命令不需要过多了解,因为对于一门面向过程的编程语言来说,结构化命令更重要,而且一些命令的帮助还能在man - manual page看到(比如
ls
命令,man ls
即可查看它的帮助,只是英文生肉可能啃不下)。
文件相关:
- ls - list:列出当前目录的文件,-a则包括隐藏文件,-l详细信息。
- cd - change directory:改变shell所在目录,上一级用..表示,比如
/ $ cd usr
/usr/ $
/usr/ $ cd ..
/ $
- rm - remove:删除文件,删除目录及其子目录下所有文件需要添加-r(recursive),强制删除添加-f(force)。
sudo rm -rf /*
$ rm [file]
- mv - move:移动,更新,重命名文件
$ mv [orgion_file] [end_file]
- cp - copy:复制文件,复制目录及其子目录下所有文件需要添加-r(recursive)
$ cp [origon_file] [end_file]
- mkdir - make directory:创建文件夹,就这么简单。
- pwd - print working directory:返回shell所在目录。
/ $ pwd
/
/ $ cd usr/share
/usr/share/ $ pwd
/usr/share/
/usr/share/ $
可执行文件相关:
- command:顾名思义,直接运行一个程序,但是-v可以返回程序路径
$ command echo Execing
Execing
$ command -v echo
/usr/bin/echo
$
- alias:设置命令别名
$ badcommand
-bash: badcommand: Command not found
$ alias badcommand='echo Command Worked!'
$ badcommand
Command Worked!
- . - source:使用当前的shell运行程序,这个没什么好说的(实际上,当我们打开一次终端的时候,bash已经在偷偷的执行了一次. ~/.bashrc了)。
- exec - execute:覆盖进程以运行新的程序,即进程PID不变,但是运行的程序变了(换汤不换药)。
$ exec echo execing
execing
[logout]
但是exec命令有一个特例,我要放在文件描述符跟重定向里面说。
系统和用户相关:
- uname - Unix name:用于查看设备架构,主机名,内核版本,跟发行版。
- id - identical:获取当前用户信息。
进程相关:
- top:打开实时进程状态。
- kill:传入一个PID数以给一个进程发送信号,不加-s或-n默认发送
SIGTERM
(以正常方式退出,较为温和的信号),更多我会在信号一章节详细说。
- ps - process staus:当前shell下进程状态。
另外,我还要说几个配套的用法/快捷键
Ctrl-C快捷键:给进程发送SIGINT
信号,这大概是用的最多的快捷键了。
$ sleep 50
^C
$
Ctrl-Z快捷键:给进程发送SIGTSTP
信号。
$ sleep 50
[1]+ Stopped sleep 50
$
我们可以在多个进程暂停,这时候就会显示[2]+、[3]+以此类推。
但是如果有暂停的进程,bash是无法退出的。
$ exit
There are stopped jobs.
$
那我们怎么回去呢?
好了,基本命令看完了,我们直接开始实战。
命令行第二式:符号和结构化语言
shell既然是一门语言,拥有结构化的语法是肯定的,我们首先从符号开始。
- :用户目录。
$ id -nu
linuxuser001
$ echo ~
/home/linuxuser001
$ cd ~
/home/linuxuser001/ $
- *:当前目录下的所有文件。
/ $ echo *
bin boot dev etc home init lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv sys tmp usr var
#:注释。
$ #这是一个注释
- !:FALSE值或上一条命令,作为FALSE值时跟false命令等价。会在返回值章节详细讲。
$ echo $?
0
$ !
$ echo $?
1
这是作为上一条命令的情况
$ !echo
$ echo $?
- ():注册一个子shell进程,并且会在括号内的命令运行完毕后自动退出。
- %:等价于fg命令。
:
:(注意这是一个冒号)TRUE值,跟true命令等价。
$ !
$ echo $?
1
$ :
$ echo $?
0
$
- \:转义符。
/ $ echo *
bin boot dev etc home init lib lib32 lib64 libx32 lost+found media mnt opt proc root run sbin snap srv sys tmp usr var
/ $ echo \*
*
/ $ echo \
>
/ $
#注意,此时它转义了回车字符
/ $ echo \
>message
message
/ $
- &:逻辑与符号或者转向后台执行符号,一个为后台执行(跟运行程序后立即按Ctrl-Z等价,可以用fg恢复),两个为and逻辑。
$ false && echo this is true!
$
$ true && echo this is true!
this is true!
$
$
:这个符号是shell语言的核心,因为它代表了shell脚本的变量,会在变量章节详细讲。(同时,美元符号自己也是个变量,记录了当前shell的PID值)。
$ var=114514
$ echo $var
114514
$ echo $$
1208
$
- |:逻辑或符号或者管道符号,一个为管道符号(用法:有STDOUT的|有STDIN的,文件描述符详细讲),两个为or逻辑。
$ false || echo this is false!
this is false!
$ true || echo this is false!
;:(注意,这是一个分号)连接符号,在一行运行多个代码块,相当于回车符号
$ echo code1 ; echo code2 ; echo code3
code1
code2
code3
$
- "":将零散的字符串视为一个整体,但是字符串接受反斜杠和美元符号转义。
- '':将零散的字符串视为一个整体,字符串不接受反斜杠和美元符号转义。
$ var=114514
$ echo "str1 $var"
str1 114514
$ echo 'str1 $var'
str1 $var
<和>:重定向符号,文件描述符详细讲。
结构化语言
让我想想,结构化语言应该先从哪里开始。。。if!
shit-code型
if command
then
[true_command]
else
if command2
[true2_command]
else
[false_command]
fi
fi
@雪碧 出来看看你写的金玉良言
新概念shit-code型(千古第一人,命令行调试大佬)
if command ; then [true_command] ; else if command2 ; then [true2_command] ; else [false_command] ; fi ; fi
能看懂一点算我输
C语言忠实者型(打死都不用elif,无法折叠的代码简直就是对我的侮辱)
if command ; then
[true_command]
else
if command2 ; then
[true2_command]
else
[false_command]
fi
fi
代码风格跟C语言确实很像
我甚至没if型(简单粗暴)
command && [true_command] || ( command2 && [true2_command] || [false_command] )
请注意,上面那个已经把子shell和逻辑符号玩得出神入化,是个大佬
- if其实是一个很强大的命令,它的command可以是变量检查,也可以是检测命令返回值,比如\
变量检查 - if (( $var compare var ))
,compare可以是==,!=,>=,<=(注意,双圆括号里面的东西要跟括号隔开空格,不然会报语法错误)\
比如
$ var=114514
$ if (( $var == 114514 )) ; then echo homo? ; fi
homo?
$ if (( $var >= 1919810 )) ; then echo true ; fi
$
检测命令返回值 - if command
,当command返回0时,if会执行then后面的语句,所以,if false会只执行else后面的语句。
for语句
- for语句怎么说呢,既简单又不简单,它的基本语法是
for argument
do
command
done
同样的,argument可以遍历也可以递增递减,比如遍历一堆字符串
for i in "str1" "str2" "str3"
do
echo $i
done
输出结果
str1
str2
str3
递增语法(递减同理,只需将<=替换成>=,++替换成–)
for ((i=1,i<=5,i++))
do
echo $i
done
输出结果
1
2
3
4
5
while和until语句
- while和until一样。都是拼接怪(if和for的拼接),它的目的是当情况为真/假时,即开始循环,直到情况相反为止
- 基本语法while !cmd和until cmd等价,你可以说until时while的另一面
while conditions
do
[loop_command]
done
等价于
until ! conditions
do
[loop_command]
done
conditions
跟if的判断方式一样,变量检测和命令返回值都可以用,进阶一点甚至能内联重定向(<)
case语句
- case用于多重分支的判断语句里面(什么?有同学说用一大堆if就可以了?我的回答是请看这里
)
- 咳咳,由于开发者不想让用户都依赖于case语句,所以case只支持in写法
- 基本语法(特别的双分号和右括号,当然,我们也可以在commands里面嵌入结构化语言)
case $var in
var1)
command1;;
#单个情况处理
var2|var3)
command2;;
#两个或多个情况都用command2
'')
command3;;
#处理空格、空变量、回车符
*)
all_out_of_expected_event;;
#上述都没执行时,这个开始生效
esac
select语句
- select语句算是最少人用的了,因为角度刁钻。它是将一堆字符串供用户选择,然后开始运行loop_command,运行完毕又返回选择界面。就这么简单,所以,和case一样,它只支持in写法
- 基本语法
select seld in "str1" "str2" "str3"
do
[loop_command]
done
因为这样,我们常常会在里面加break
$ select i in str1 str2 str3 ; do echo $i ; done
1) str1
2) str2
3) str3
#? 1
str1
#? 2
str2
#? 4
#?
^C
$
function语句
- 终于来到重头戏了,function语句可以让用户创建一个函数,并且接受传入参数(后面我会讲到)
- 基本语法
function func_name {
commands
}
简单但是很实用
好了,一些结构化语言讲完了,接下来看看特殊语法
test命令和[]符号
- 这是一个非常强大的命令,它可以检测变量、文件情况,用户权限等
- test命令与[]符号等价
- if test conditions => if [ conditions ]
数值检测:
test var1 -eq | -ne | -gt | -lt | -ge | -le var2
,一共六种情况
参数 | 作用 |
-eq:equal | 相等 |
— | — |
-ne:not equal | 不相等 |
— | — |
-gt:greater than | 大于 |
— | — |
-lt:less than | 小于 |
— | — |
-ge:greater or equal | 大于等于 |
— | — |
-le:less or equal | 小于等于 |
文件检测:
test -b | -c | -d | -e | -f | -L | -p | -s | -S [filename]
,一共八种情况
参数 | 作用 |
-b:block | 检测是否为块设备文件 |
-c:character | 检测是否为字符设备文件 |
-d:directory | 检测是否为目录 |
-e:exist | 检测是否存在 |
-f:file | 检测是否存在且为普通文件 |
-p:pipe | 检测是否为管道文件 |
-s:? | 检测文件是否非空 |
-S:socket | 检测是否为套接字文件 |
字符串检测:
单变量:
test -z | -n $str
双变量:
test $str1 \> | \< | == | != $str2
-z:?,字符串为空时返回TRUE值
-n:?,字符串不为空时返回TRUE值
所以,! test -z
=> test -n
[ -z $str ]
返回值:
传入参数:
$*
和$@
变量
$*
以整个字符串的方式获取所有参数
$@
以多个字符串的方式获取(也就是说,可以遍历)
shift命令
- shift可以将整体参数往左移一位,相当于删除原有的$1,然后$2变$1,$3变$2,以此类推
function args {
shift
echo $@
}
$ args 1 2 3 4
2 3 4
$
break
、continue
和exit
:
- break和continue只能出现在do-done里面,而exit可以出现在任何地方
- break用于跳出循环,接受一个参数(整数),作跳出的层数,不添加默认为1,跳出最内层循环
- continue用于跳过一次循环,例
for ((i=0,i<=5,i++))
do
if [ $i -eq 3 ]
then
continue
fi
done
输出结果
1
2
4
5
- exit用于退出shell,如果退出的是子shell,则接受一个参数作为返回值,默认为0
$ ( exit 0 )
$ echo $?
0
$ ( exit 2 )
$ echo $?
2
$
好了,结构化语言讲完了,我们接着下一个
命令行第三式:重定向和文件描述符
- 在Linux中,一切设备皆文件这句话终于能在这里体现出来了
- 首先是重定向,bash接受我们键盘的输入,并且将结果打印输出到显示器。在Linux系统中,键盘和显示器都是文件,那么我们有没有什么办法可以改变bash指定的文件呢(如报错、日志收集)?
- 这个章节,cat全程最佳!
输出重定向:最简单的重定向
- 输出重定向,就是将打印得到的结果输出给其他文件,用>符号指定要输出到的文件,例如
$ echo LU001 yyds! > file
$ cat file
LU001 yyds!
$
但是这样有个坏处,它会覆盖原来的文件内容,而导致一些损失。这时候我们可以叠加重定向,用>>
代替>
$ cat file
LU001 yyds!
$ echo xb6868 >> file
$ cat file
LU001 yyds!
xb6868
$
输入重定向:键盘的代替品
- 输入重定向分为文件输入重定向和内联重定向也就是一个从文件获取信息,另一个以另一种方式获取键盘输入的信息
- 在某些情况下,文件输入重定向和内联重定向等价
例如普通情况下是cat作为程序直接读取文件
$ cat file
LU001 yyds!
xb6868
$
文件输入重定向则是将文件内容映射到临时生成的文件描述符,并且cat读取的是文件描述符里面的内容,相当于中间商
$ cat < file
LU001 yyds!
xb6868
$
但是,当我们输入了两个小于号,那么性质就变了,文件描述符不再从文件获取内容,转而从键盘了。而在两个小于号后面的第一个字符串,被视为分界符,也就是我们常见的**EOF(End Of File)**
在示例中,file可以是任何字符串,只要在后面的>提示符输入相同的字符串或者按下Ctrl-D(Ctrl-D充当EOF),即可退出cat并且输出信息
$ cat << file
>LU001 yyds!
>xb6868
>file
LU001 yyds!
xb6868
$
文件描述符:文件大一统的天下
- 上面说到,Linux一切设备皆文件,而文件描述符则是Linux用于处理流数据的字符设备,所以我们可以读取,生成,打开,关闭一个文件描述符
- 如果我们运行
ls -l /proc/self/fd
,可以看到0 1 2,这些文件描述符全部都连接到了$(tty)
所在的位置,而当我们cat这个文件,它就开始接受键盘输入,这跟我们直接运行cat是一样的
- 下面是对Linux常驻文件描述符的介绍
range | 介绍 |
0 - STDIN(Standard Input) | 标准输入 |
1 - STDOUT(Standard Output) | 标准输出 |
2 - STDERR(Standard Error) | 标准错误 |
其中,bash从0接受键盘输入,将1和2打印输出到显示器,当然,文件描述符也可以重定向,比如将错误信息重定向至文件
$ ls /root
ls: cannot open directory '/root': Permission denied
$ ls /root 2>error_file
$ cat error_file
ls: cannot open directory '/root': Permission denied
$
但是,Linux常驻的文件描述符只有0 1 2,还能不能再多一点?
- 事实上,Linux早已看透我们的想法,并且支持可用的0-9十个描述符,减去三个常驻,供用户使用的就有七个
可是/proc/self/fd只有三个啊,那怎么生成呢?
- 还记得之前的exec命令吗?它可以狸猫换太子,将程序直接缓冲到shell的内存段,如果让它配合重定向,只替换重定向的代码段,不就成了吗?
那也就是说,exec 2>3
是可行的!
但是…好像只生成了一个名字叫3的文件,没错,这就是我要提醒的,写入文件描述符需要在数字前加上&
,所以,命令应该是<b><font color=red>exec 2>&3</font></b>(当然,文件描述符也可以和文件互通)
现在,把3这个文件删掉,再试试ls /root,我们就会惊奇的发现,既没有3这个文件生成出来,也没有输出错误
好了,这个描述符玩腻了,那么怎么关掉它呢?
很简单,exec 3>-
即可
管道:接暗号
当我们看完上面的部分之后,管道就变得简单多了
信号
我们只需要用空字符串作为命令的传入参数即可
trap '' SIGTSTP
man 7 signal:
- 这是Linux信号较为官方的帮助页面,本人很懒,所以只能精讲
- 前面的介绍历史什么的可以跳过,然后到Signal dispositions这里,他说signal在应对这么多信号的措施
Linux应对信号有五种措施,分别是Term、Ign、Core、Stop和Cont
DISP | 介绍 |
Term - Terminal | 终结进程 |
Ign - Ignore | 无视风险继续运行(? |
Core - Core | 终结进程并转储核心 |
Stop - Stop | 暂停进程 |
Cont - Continue | 针对Stop的继续进程 |
“The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.”
高效编程
- 这一章节是扩展章节,主要是讲如何写出别人看了都说好的脚本
(@雪碧)
环境的设置:
写好了一个脚本,但是开篇所说的各sh之间的语法差异导致脚本无法正常运行,那怎么办呢?
- 我们只需要在脚本第一行添加
#!/path/to/shell_prompt
即可
例如我用的是bash,那么我应该在脚本第一行添加
#!/bin/bash
注释:
注释真的很重要,不过既然是面向过程的shell,注释可以不写,但是命令的意图一定要明确体现
变量和函数名:
千万不要嫌麻烦,变量随便用个abc写,到时候debug会很难受
缩进:
缩进是一个很影响代码观感的重要因素,一个好的脚本应该写成这样
function file_path_scan
{
while true
do
dialog --inputbox "输入你的$file 的绝对路径" 40 200 2> path.log
if (( $? == 1 )); then
exit
else
export sel=$(cat path.log)
if test -z $sel; then
dialog --msgbox "请输入正确的路径" 40 200
rm path.log
else
if ! test -d $sel; then
dialog --msgbox "没有该目录" 40 200
rm path.log
else
if ! echo $sel[$(echo ${#sel})]|grep "/"; then
export sel[$[$(echo ${#sel})+1]]="/"
fi
file_find2 $filename
export find=$(cat find.txt)
if test -z "$find"; then
dialog --msgbox "在$sel 这个目录下找不到$file" 40 200
rm find.txt path.log
unset sel
else
file_find
rm path.log
fi
fi
fi
fi
done
}
摘自Dialog-Qemu-Script-Creater的filepr.sh
而不是这样
function file_path_scan
{
while true
do
dialog --inputbox "输入你的$file 的绝对路径" 40 200 2> path.log
if (( $? == 1 )); then
exit
else
export sel=$(cat path.log)
if test -z $sel; then
dialog --msgbox "请输入正确的路径" 40 200
rm path.log
else
if ! test -d $sel; then
dialog --msgbox "没有该目录" 40 200
rm path.log
else
if ! echo $sel[$(echo ${#sel})]|grep "/"; then
export sel[$[$(echo ${#sel})+1]]="/"
fi
file_find2 $filename
export find=$(cat find.txt)
if test -z "$find"; then
dialog --msgbox "在$sel 这个目录下找不到$file" 40 200
rm find.txt path.log
unset sel
else
file_find
rm path.log
fi
fi
fi
fi
done
}
对了,shell是不会担心我们用的是空格还是\t(Tab键)缩进,只有python这种麻烦的语言才会!请务必不辞麻烦多用Tab键!
—
EOF