Linux 下Python 脚本编写的“奇技淫巧“

Posted 山河已无恙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 下Python 脚本编写的“奇技淫巧“相关的知识,希望对你有一定的参考价值。

写在前面

  • 对于自动化运维来讲Python是一个利器
  • 常用的自动化运维工具Ansible就是通过python编写
  • 博文为《Python Cookbook》读书笔记整理而来
  • 涉及的内容都是编写python运维脚本常用的一些知识点及Demo
  • 理解不足小伙伴帮忙指正

生命完美的答案,无非走过没有遗憾 —《天蓝》


脚本编程与系统管理

解析命令行选项

如何能够解析脚本运行命令行选项(位于 sys.argv 中)

argparse 模块可被用来解析命令行选项

常用来定义一个脚本的说明文档,一般我们写python脚本会通过if..else 的方式来提供一个脚本说明文档,python不支持switch。所有很麻烦,其实,我们可以通过argparse来编写说明文档。

我们来看看执行一个python脚本

对于熟悉Linux的小伙伴下面的文档在熟悉不过了,这个一个标准Linxu软件包的说明文档,文档中定义是软件包的说明

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$./demo.py -h
usage: demo.py [-h] -p pattern [-v] [-o OUTFILE] [--speed slow,fast]
               [filename [filename ...]]

Search some files

positional arguments:
  filename

optional arguments:
  -h, --help            show this help message and exit
  -p pattern, --pat pattern
                        text pattern to search for
  -v                    verbose mode
  -o OUTFILE            output file
  --speed slow,fast   search speed
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$

来看看这个脚本是如何编写的

#!/usr/bin/env python3


import argparse
# 脚本的描述
parser = argparse.ArgumentParser(description='Search some files')
# 脚本接收的全部参数,用`filenames`接收
parser.add_argument(dest='filenames', metavar='filename', nargs='*')

# 脚本接收
parser.add_argument('-p', '--pat', metavar='pattern', required=True,
                    dest='patterns', action='append',
                    help='text pattern to search for')
parser.add_argument('-v', dest='verbose', action='store_true',
                    help='verbose mode')
parser.add_argument('-o', dest='outfile', action='store',
                    help='output file')
parser.add_argument('--speed', dest='speed', action='store',
                    choices='slow', 'fast', default='slow',
                    help='search speed')
args = parser.parse_args()
# Output the collected arguments
print(args.filenames)
print(args.patterns)
print(args.verbose)
print(args.outfile)
print(args.speed)

为了解析命令行选项, 首先要创建一个ArgumentParser实例, 并使用add_argument() 方法声明你想要支持的选项。在每个add-argument()调用中:

dest参数指定解析结果被指派给属性的名字。 metavar 参数被用来生成帮助信息。

action 参数指定跟属性对应的处理逻辑,通常的值为 store , 被用来存储某个值或将多个参数值收集到一个列表中

nargs 参数收集所有剩余的命令行参数到一个列表中。在本例中它被用来构造一个文件名列表

parser.add_argument(dest='filenames',metavar='filename', nargs='*')
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$python3 demo.py  -p spam --pat=eggs  foo.txt bar.txt
['foo.txt', 'bar.txt']
['spam', 'eggs']
False
None
slow
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$

action='store_true' 根据参数是否存在来设置一个 Boolean 标志:

parser.add_argument('-v', dest='verbose', action='store_true', help='verbose mode')
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$python3 demo.py -v -p spam --pat=eggs foo.txt bar.txt
['foo.txt', 'bar.txt']
['spam', 'eggs']
True
None
slow

action='store' 参数接受一个单独值并将其存储为一个字符串

parser.add_argument('-o', dest='outfile', action='store', help='output file')
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$python3 demo.py -v -p spam --pat=eggs -o liruilong  foo.txt bar.txt
['foo.txt', 'bar.txt']
['spam', 'eggs']
True
liruilong
slow
  • action='append' 参数说明允许某个参数重复出现多次,并将它们追加到一个列表中去。
  • required 标志表示该参数至少要有一个-p --pat 表示两个参数名形式都可使用。
parser.add_argument('-p', '--pat', metavar='pattern', required=True,
                    dest='patterns', action='append',
                    help='text pattern to search for')
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$python3 demo.py  -p spam   foo.txt bar.txt
['foo.txt', 'bar.txt']
['spam']
False
None
slow
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$python3 demo.py  --pat=eggs  foo.txt bar.txt
['foo.txt', 'bar.txt']
['eggs']
False
None
slow
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$python3 demo.py  -p spam --pat=eggs  foo.txt bar.txt
['foo.txt', 'bar.txt']
['spam', 'eggs']
False
None
slow

如果一个都没有,会提示缺少参数 -p/--pat

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$python3 demo.py   foo.txt bar.txt
usage: demo.py [-h] -p pattern [-v] [-o OUTFILE] [--speed fast,slow]
               [filename [filename ...]]
demo.py: error: the following arguments are required: -p/--pat
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$

choices='slow', 'fast', 参数说明接受一个值,但是会将其和可能的选择值做比较,以检测其合法性:

parser.add_argument('--speed', dest='speed', action='store',
                    choices='slow', 'fast', default='slow',
                    help='search speed')
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$python3 demo.py  --pat=eggs --speed 123  foo.txt bar.txt
usage: demo.py [-h] -p pattern [-v] [-o OUTFILE] [--speed slow,fast]
               [filename [filename ...]]
demo.py: error: argument --speed: invalid choice: '123' (choose from 'slow', 'fast')
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$python3 demo.py  --pat=eggs --speed fast  foo.txt bar.txt
['foo.txt', 'bar.txt']
['eggs']
False
None
fast
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$

一旦参数选项被指定,你就可以执行parser.parse()方法了。它会处理sys.argv的值并返回一个结果实例。每个参数值会被设置成该实例中add_argument()方法的 dest 参数指定的属性值。

还很多种其他方法解析命令行选项。可以会手动的处理 sys.argv 或者使用 getopt 模块。但是,如果你采用本节的方式,将会减少很多冗余代码,底层细节argparse 模块已经帮你处理了。你可能还会碰到使用optparse库解析选项的代码。尽管 optparse 和 argparse 很像,但是后者更先进,因此在新的程序中你应该使用它。

运行时弹出密码输入提示

你写了个脚本,运行时需要一个密码。此脚本是交互式的,因此不能将密码在脚本中硬编码,而是需要弹出一个密码输入提示,让用户自己输入。

Python 的 getpass 模块正是你所需要的。你可以让你很轻松的弹出密码输入提示,并且不会在用户终端回显密码。

#!/usr/bin/env python3
# -*- encoding: utf-8 -*-

import getpass

def svc_login(user, passwd):
    return user == passwd

user = getpass.getuser()
passwd = getpass.getpass()
if svc_login(user, passwd):
    print('Yay!')
else:
    print('Boo!')

代码中getpass.getuser()不会弹出用户名的输入提示。它会根据该用户的 shell 环境或者会依据本地系统的密码库(支持 pwd 模块的平台)来使用当前用户的登录名

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$./pass.py
Password: #root
Yay!

通过重定向/管道/文件接受输入

在bash中编写pytohn脚本接收外部数据的方式,一般情况下,对于一般变量,我们用命令行变量的方式比较多(手动的处理 sys.argv ),对于文件内容或者bash命令输出直接通过脚本内部获取需要的数据。

其实python 脚本也可以用其他方式来接收 传递给他的文件数据或者bash命令输出,包括将命令行的输出通过管道传递给该脚本、重定向文件到该脚本,或在命令行中传递一个文件名文件名列表给该脚本。

这里通过 Python 内置的 fileinput 模块,可以实现重定向,管道,文佳输出的方式传递数据到脚本内部

#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
"""
@File    :   filein.py
@Time    :   2022/05/01 06:05:43
@Author  :   Li Ruilong
@Version :   1.0
@Contact :   1224965096@qq.com
@Desc    :   None
"""

# here put the import lib

import fileinput
with fileinput.input() as f_input:
    for line in f_input:
        print("脚本输出", line, end='')

使用fileinput.input()方法可以获取当前输入脚本的数据,脚本里面用一个FileInput迭代器接收

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$vim filein.py
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$chmod +x filein.py

文件直接接收

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$./filein.py /etc/passwd
脚本输出 root:x:0:0:root:/root:/bin/bash
脚本输出 bin:x:1:1:bin:/bin:/sbin/nologin
脚本输出 daemon:x:2:2:daemon:/sbin:/sbin/nol
。。。。

重定向接收

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$./filein.py <  /etc/passwd
脚本输出 root:x:0:0:root:/root:/bin/bash
脚本输出 bin:x:1:1:bin:/bin:/sbin/nologin
。。。。。。

管道方式接收

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$df -h
文件系统        容量  已用  可用 已用% 挂载点
/dev/sda1       150G   22G  129G   15% /
devtmpfs        983M     0  983M    0% /dev
tmpfs           993M     0  993M    0% /dev/shm
tmpfs           993M   17M  976M    2% /run
tmpfs           993M     0  993M    0% /sys/fs/cgroup
overlay         150G   22G  129G   15% /var/lib/docker/overlay2/9fbd33d3485f02eadef6907a5b4eaead4a384684b66c572d822a2942a82ca0d5/merged
overlay         150G   22G  129G   15% /var/lib/docker/overlay2/85ff22ccaf2db68a0a863bc404d79d72fa6c8744424f50ba8fb6bfa83d56b56a/merged
tmpfs           199M     0  199M    0% /run/user/0
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$df -h |  ./filein.py
脚本输出 文件系统        容量  已用  可用 已用% 挂载点
脚本输出 /dev/sda1       150G   22G  129G   15% /
脚本输出 devtmpfs        983M     0  983M    0% /dev
脚本输出 tmpfs           993M     0  993M    0% /dev/shm
脚本输出 tmpfs           993M   17M  976M    2% /run
脚本输出 tmpfs           993M     0  993M    0% /sys/fs/cgroup
脚本输出 overlay         150G   22G  129G   15% /var/lib/docker/overlay2/9fbd33d3485f02eadef6907a5b4eaead4a384684b66c572d822a2942a82ca0d5/merged
脚本输出 overlay         150G   22G  129G   15% /var/lib/docker/overlay2/85ff22ccaf2db68a0a863bc404d79d72fa6c8744424f50ba8fb6bfa83d56b56a/merged
脚本输出 tmpfs           199M     0  199M    0% /run/user/0
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$

fileinput.input() 创建并返回一个FileInput类的实例,该实例可以被当做一个上下文管理器使用。因此,整合起来,如果我们要写一个打印多个文件输出的脚本,那么我们需要在输出中包含文件名和行号

>>> import fileinput
>>> with fileinput.input("/etc/passwd") as f:
...     for line in f:
...             print(f.filename(),f.fileno(),f.lineno(),line,end='')
...
/etc/passwd 3 1 root:x:0:0:root:/root:/bin/bash
/etc/passwd 3 2 bin:x:1:1:bin:/bin:/sbin/nologin
/etc/passwd 3 3 daemon:x:2:2:daemon:/sbin:/sbin/nologin
/etc/passwd 3 4 adm:x:3:4:adm:/var/adm:/sbin/nologin
/etc/passwd 3 5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
/etc/passwd 3 6 sync:x:5:0:sync:/sbin:/bin/sync

执行外部命令并获取它的输出

你想执行一个外部命令并以 Python 字符串的形式获取执行结果。

使用subprocess.check_output()函数。

#!/usr/bin/env python3
# -*- encoding: utf-8 -*-

import subprocess
out_bytes = subprocess.check_output(['netstat','-a'])
out_text = out_bytes.decode('utf-8')
print(out_text)


执行下试试

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$./py_sh.py
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 localhost:2379          0.0.0.0:*               LISTEN
tcp        0      0 vms55.rhce.cc:2379      0.0.0.0:*               LISTEN
tcp        0      0 localhost:2380          0.0.0.0:*               LISTEN
tcp        0      0 vms55.rhce.cc:2380      0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:webcache        0.0.0.0:*               LISTEN
tcp        0      0 0.0.0.0:http            0.0.0.0:*               LISTEN

如果被执行的命令以非零码返回,就会抛出异常。下面的例子捕获到错误并获取返回码:

try:
    out_bytes = subprocess.check_output(['cmd','arg1','arg2'])
except subprocess.CalledProcessError as e:
    out_bytes = e.output # Output generated before error
    code = e.returncode # Return code

默认情况下,check_output() 仅仅返回输入到标准输出的值。如果你需要同时收集标准输出和错误输出,使用stderr参数:

out_bytes = subprocess.check_output(['cmd','arg1','arg2'],stderr=subprocess.STDOUT)

如果你需要用一个超时机制来执行命令,使用 timeout 参数:

try:
    out_bytes = subprocess.check_output(['cmd','arg1','arg2'], timeout=5)
except subprocess.TimeoutExpired as e:
    ....

通常来讲,命令的执行不需要使用到底层 shell 环境(比如 sh、bash)。一个字符串列表会被传递给一个低级系统命令,比如 os.execve()

如果你想让命令被一个shell 执行,传递一个字符串参数,并设置参数 shell=True . 有时候你想要Python去执行一个复杂的 shell 命令的时候这个就很有用了,比如管道流、I/O 重定向和其他特性。例如:

out_bytes = subprocess.check_output('grep python | wc > out', shell=True)

是在 shell 中执行命令会存在一定的安全风险,特别是当参数来自于用户输入时。这时候可以使用 shlex.quote() 函数来将参数正确的用双引用引起来。

使用 check_output() 函数是执行外部命令并获取其返回值的最简单方式。但是,如果你需要对子进程做更复杂的交互,比如给它发送输入,你得采用另外一种方法。这时候可直接使用subprocess.Popen类。

#!/usr/bin/env python3
# -*- encoding: utf-8 -*-

import subprocess
# Some text to send
text = b'''
hello world
this is a test
goodbye
'''
# Launch a command with pipes
p = subprocess.Popen(['wc'],
                     stdout=subprocess.PIPE,
                     stdin=subprocess.PIPE)
# Send the data and get the output
stdout, stderr = p.communicate(text)
# To interpret as text, decode
out = stdout.decode('utf-8')
err = stderr.decode('utf-8')

关于子进程,简单来看下

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$(pwd;echo $BASH_SUBSHELL;ps --forest)
/root/python_demo
1
   PID TTY          TIME CMD
  9324 pts/0    00:00:00 bash
 49906 pts/0    00:00:00  \\_ bash
 49907 pts/0    00:00:00      \\_ ps
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$pwd;echo $BASH_SUBSHELL;ps --forest
/root/python_demo
0
   PID TTY          TIME CMD
  9324 pts/0    00:00:00 bash
 49908 pts/0    00:00:00  \\_ ps
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$

也可以进程列表同协程结合的方式。你既可以在子shell中 进行繁重的处理工作,同时也不会让子shell的I/O受制于终端。

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$coproc (sleep 10;ps --forest;sleep 10;ps --forest)
[1] 50326
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$jobs
[1]+  运行中               coproc COPROC ( sleep 10; ps --forest; sleep 10; ps --forest ) &

如果直接丢到后台会自动在终端输出IO

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$( sleep 10; ps --forest; sleep 10; ps --forest ) &
[1] 50335
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$ps --forest
   PID TTY          TIME CMD
  9324 pts/0    00:00:00 bash
 50335 pts/0    00:00:00  \\_ bash
 50336 pts/0    00:00:00  |   \\_ sleep
 50337 pts/0    00:00:00  \\_ ps
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$   PID TTY          TIME CMD
  9324 pts/0    00:00:00 bash
 50335 pts/0    00:00:00  \\_ bash
 50340 pts/0    00:00:00      \\_ ps

[1]+  完成                  ( sleep 10; ps --forest; sleep 10; ps --forest )
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$

subprocess 模块对于依赖 TTY 的外部命令不合适用。例如,你不能使用它来自动化一个用户输入密码的任务(比如一个 ssh 会话)。这时候,你需要使用到第三方模块了,比如基于著名的 expect 家族的工具(pexpect 或类似的)(pexpect可以理解为Linux下的expect的Python封装、通过pexpect可以实现对ssh、ftp、passwd、telnet等命令行进行自动交互,而无需人工干涉来达到自动化的目的。比如我们可以模拟一个FTP登录时所有交互,包括输入主机地址、用户名、密码、上传文件等,待出现异常还可以进行尝试自动处理。)

终止程序并给出错误信息

你想向标准错误打印一条消息并返回某个非零状态码来终止程序运行

通过 pythonraise SystemExit(3)命令可以主动抛出一个错误,通过sys.stderr.write将命令写到标准的输出端

#!/usr/bin/env python3

import sys
sys.stderr.write('It failed!\\n')
raise  SystemExit(3)
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$vim err.py
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$./err.py
It failed!
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$echo $?
3

直接将消息作为参数传给SystemExit(),那么你可以省略其他步骤

#!/usr/bin/env python3

raise SystemExit('It failed!')

抛出一个 SystemExit 异常,使用错误消息作为参数,它会将消息在sys.stderr中打印,然后程序以状态码1退出

┌──[root@liruilongs.github.io]-[~/python_demo]
└─$./err.py
It failed!
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$echo $?
1

获取终端的大小

你需要知道当前终端的大小以便正确的格式化输出。

使用 os.get terminal size() 函数来做到这一点。

#!/usr/bin/env python3
# -*- encoding: utf-8 -*-

import os
sz = os.get_terminal_size()
print(sz)
print(sz.columns)
print(sz.lines)
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$./tim.py
os.terminal_size(columns=99, lines=30)
99
30
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$./tim.py
os.terminal_size(columns=165, lines=30)
165
30
┌──[root@liruilongs.github.io]-[~/python_demo]
└─$

复制或者移动文件和目录

复制或移动文件和目录,但是又不想调用 shell 命令。

shutil 模块有很多便捷的函数可以复制文件和目录。使用起来非常简单

#!/usr/bin/env python3
# -*- encoding: utf-8 -*-

import shutil
# Copy src to dst. (cp src dst)
shutil.copy(src, dst)
# Copy files, but preserve metadata (cp -p src dst)
shutil.copy2(src, dst)
# Copy directory tree (cp -R src dst)
shutil.copytree(src, dst)
# Move src to dst (mv src dst)
shutil.move(src, dst)

这里不多讲,熟悉Linux的小伙伴应该不陌生。

默认情况下,对于符号链接这些命令处理的是它指向的东西文件。例如,如果源文件是一个符号链接,那么目标文件将会是符号链接指向的文件。如果你只想复制符号链接本身,那么需要指定关键字参数 follow_symlinks

shutil.copytree以上是关于Linux 下Python 脚本编写的“奇技淫巧“的主要内容,如果未能解决你的问题,请参考以下文章

Linux 下Python 脚本编写的&amp;quot;奇技淫巧&amp;quot;

奇技淫巧linux下各种反弹shell脚本

校园生活奇技淫巧——羽毛球场预订脚本的编写篇

linux shell编程-bash的奇技淫巧

如何在linux环境下添加python模块路径

Linux:Python编写Linux自定义命令工具