shell入门教程-其实你也能看懂别人写的脚本!
标签搜索

shell入门教程-其实你也能看懂别人写的脚本!

Linux User001
2022-07-13 / 0 评论 / 36 阅读 / 正在检测是否收录...

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!
    • type:判断程序是bash内置还是外部程序还是别名
    $ type cd
    cd is a shell builtin
    $ alias badcommand=command
    $ type badcommand
    badcommand is aliased to 'command'
    $ type nano
    nano is /bin/nano
  • . - 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.
$

那我们怎么回去呢?

  • jobs:查看进程的工作状态。

    $ jobs
    [1]+  Stopped                sleep 50
  • fg - foreground:将暂停的进程调至前台继续运行。

    $ fg
  • nohup - no hang up:不挂起进程,在后台注册一个新shell用于运行程序,当前shell没有任何影响(关闭该shell不会杀死那个进程,因为nohup已经将它带去了新的地方)。

好了,基本命令看完了,我们直接开始实战。

命令行第二式:符号和结构化语言

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!

  • if是shell的简单的判断命令,它的基本语法是

    if command
    then
      [true_command]
    elif command2
    then
      [true2_command]
    else
      [false_command]
    fi
    • 配合分号转义回车,结构语言的结构可以千变万化
  • 举几个例子(

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 ]

返回值:

  • 返回值相当简单,$?变量记录了上一条命令的返回值,只有0才是真,其它都为假
  • return命令:只能出现在function结构内,用于返回一个返回值(其他情况只能用exit代替,比如exit 0),相当于改写$?变量,范围是0-255,超出不会报错

    function return_test {
        return 264
    }
    $ return_test
    $ echo $?
    9
    $
  • 看吧,shell很聪明,它返回了超出的数与255的差值,264-255=9

传入参数:

  • shell可以处理传入参数,并且会将传入参数自动视为变量

    function args {
      echo $1 $2
    }
    $ args str1 str2
    str1 str2
    $ args str1
    str1
    $
  • 不难发现,传入参数的第一个变成了$1变量,第二个$2,以此类推
  • 但是有时候我们并不知道传入了几个参数,那么如何获取全部传入参数呢?

$*$@变量

  • $*以整个字符串的方式获取所有参数
  • $@以多个字符串的方式获取(也就是说,可以遍历)

shift命令

  • shift可以将整体参数往左移一位,相当于删除原有的$1,然后$2变$1,$3变$2,以此类推

    function args {
        shift
        echo $@
    }
    
  • 3 4
    $

  • 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的文件,没错,这就是我要提醒的,写入文件描述符需要在数字前加上&,所以,命令应该是exec 2>&3(当然,文件描述符也可以和文件互通)

现在,把3这个文件删掉,再试试ls /root,我们就会惊奇的发现,既没有3这个文件生成出来,也没有输出错误

好了,这个描述符玩腻了,那么怎么关掉它呢?

很简单,exec 3>-即可

管道:接暗号

当我们看完上面的部分之后,管道就变得简单多了

  • 管道就是Linux将文件描述符用得出神入化的例子,它将前面命令产生的数据流转到标准输入设备然后让后面的命令读取
    比如我们的grep

    ls | grep "file"

    就等价于

    grep "file" <$(ls)

    其实我们拆开管道来看,也可以这么理解

    ls >&0 | grep "file" <&0

    对哦,我差点忘了上面的结构语言重定向,那我就粗略讲讲吧

  • 结构语言中,只有function、for和select不支持内联重定向,其他的都是重定向到结构检测头或者里面需要获取输入的命令,比如

    while read line
    do
      echo $line
    done < file
    
    if grep "info"
    then
      echo pass
    else
      echo failed
    fi < file

    好了,这一章我们也算是学完了

信号

  • 信号是Linux管理进程,检测进程状态的方式,如果没有信号,那么进程将会变得杂乱无章

    trap命令:

  • trap是一个用来拦截信号的命令,它接受两个参数,示例

    trap commands signals
  • signals指定了它要拦截的信号,commands是它拦截信号后要执行的命令
  • 跟exec一样,它也会替换代码段,将本应该这样处理信号的方式替换
    示例

    $ trap 'echo Ctrl-Z trapped' SIGTSTP
    $ sleep 50
    
    ^Z Ctrl-Z trapped
    

    注意,此时sleep仍在前台运行
    那我们该怎么停止它的拦截行为呢?

我们只需要用空字符串作为命令的传入参数即可

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的继续进程
  • 信号我们只挑一些比较重要常见的来讲
  • SIGSTOPSIGTSTP:这两个都是暂停进程的信号,不同的是,SIGSTOP不能被trap拦截(年轻脚本不要太气盛!)
  • SIGCONT:相应的,这个信号能让刚接受上述信号的进程恢复运行
  • SIGTERMSIGKILLSIGINT:这三个都能中止程序,但是同样的,SIGKILL也是不能被拦截的,而SIGINT则是因为由键盘Ctrl-C产生,所以只能中断前台进程
  • SIGTTINSIGTTOU:这是进程在后台运行时(command &),如果有读写终端设备的行为(参考文件描述符),终端设备会毫不留情的暂停它,然后要用户将它转到前台运行。

    $ (sleep 3 ; cat) &
    [1] 832
    $ 等待三秒后按下回车键
    
    [1]+  Stopped            ( sleep 3; cat )
    $
  • 作者尝试拦截它结果发现信号触发并不会执行trap里面的命令,[1]+提示也没有,但是进程还是暂停了(由此可见,终端设备给它发的是SIGSTOP信号)
  • SIGCHLD:检测到子进程被终结之后(温和的退出或者粗暴的扼杀),这个信号会发送给父进程,默认措施是Ign,可以拦截

    $ trap 'echo Sub-Shell Terminated!' SIGCHLD
    $ (exit)
    Sub-Shell Terminated!
    $
  • 最后,从manual page摘抄一句原话:

"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

1

评论 (0)

取消