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登录时所有交互,包括输入主机地址、用户名、密码、上传文件等,待出现异常还可以进行尝试自动处理。)
终止程序并给出错误信息
你想向标准错误打印一条消息并返回某个非零状态码来终止程序运行
通过 python
的raise 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 脚本编写的“奇技淫巧“的主要内容,如果未能解决你的问题,请参考以下文章