Python 搞定ls命令功能 - argparse 模块


实现功能

  1. 实现ls 命令功能-l 、 -a 、 -a 和–all 、-h选项
  2. 显示路径下的文件列表
  3. -a 和–all 显示包含 “.”'开头的文件
  4. -l 显示详细列表显示
  5. -h 和 -l 配合, 个性化显示文件大小, 如1K, 1G, 1T,等, 可以按照1G = 1000M处理
  6. 类型字符
    - c 字符
    - d 目录
    - 普通文件
    - l 软连接
    - b块设备
    - s socket文件
    - p pipe文件, 即FIFO

按照文件名排序输出, 要求详细列表显示时, 时间可以按照"年-月-日 时:分:秒"格式显示, 如下图

-rw-rw-r– 1 python python 5 2019 - 5 - 7 00:07:00 test.txt
mode 硬链接 属主 属组 字节 时间 文件名

解决这个问题之前先了解下argparse 模块做起来更轻松


argparse 模块

  • 从3.2 版本开始Python 提供了参数分析的功能模块 - argparse, 解决给程序传递参数的问题

参数分类

一个可执行文件或者脚本都可以接收参数

$ ls -l /etc
# /etc 是位置参数
# -l 是短选项
  • 位置参数 : 参数放在那里就要对应一个参数位置
  • 选项参数 : 必须通过前面是 "-“的短选项或者”–"的长选项, 后面的才算该选项的参数, 当然选项后面也可以没有参数

上例中, /etc 对应的就是位置参数, -l 是选项参数

基本解析

import argparse

parser = argparse.ArgumentParser()  # 获取参数解析器
args = parser.parse_args()  # 分析参数
parser.print_help() # 打印帮助

运行结果

usage: Python.py [-h]

optional arguments:
  -h, --help  show this help message and exit
  • argparse 不仅仅做了参数的定义和解析, 还自动生成了帮助信息尤其是usage , 可以看到现在定义的参数是否是自己想要的

解析器的参数

参数名称 说明
prog 程序的名字, 缺省使用sys.argv[0] 的basename
add_help 自动为解析器增加-h 和 --help 选项, 默认True
description 为程序增加描述
import argparse

parser = argparse.ArgumentParser(prog='ls', add_help=True, description='list directory contents')  
args = parser.parse_args()  
parser.print_help() 

运行结果

usage: ls [-h]

list directory contents

optional arguments:
  -h, --help  show this help message and exit

感觉稍微有那么一点样子了 …

位置参数解析

考虑到ls 基本功能是解决目录内容打印, 打印的时候应该指定目录的路径, 需要位置参数

import argparse

parser = argparse.ArgumentParser(prog='ls', add_help=True, description='list directory contents')  # 获取参数解析器
parser.add_argument('path')

args = parser.parse_args()  # 分析参数
parser.print_help()  # 打印帮助

运行结果

usage: ls [-h] path
ls: error: the following arguments are required: path
  • 这里拿到的结果出现了错误, 但这个错误正好告诉我们需要输入path 对应的位置参数
  • " ls [-h] path " 中-h 为帮助选项,可有可无, path 为位置参数, 必须提供, 下面我们解决传参问题 …

传参

parse_args(args=None, namespace=None)

args为参数列表, 是一个可迭代对象, 内部会把可迭代对象转成list . 如果为None 则使用命令行传入参数, 非None 则使用args 参数的可迭代对象

import argparse

parser = argparse.ArgumentParser(prog='ls', add_help=True, description='list directory contents')  # 获取参数解析器
parser.add_argument('path')	# 位置参数输入

args = parser.parse_args(('/etc', ))  # 分析参数, 同时传入可迭代参数
print(args, args.path)  # 打印名词空间收集的参数
parser.print_help()  # 打印帮助

运行结果

Namespace(path='/etc') /etc
usage: ls [-h] path

list directory contents

positional arguments:
  path

optional arguments:
  -h, --help  show this help message and exit
  • Namespace(path=’/etc) 里面的path参数储存在了一个Namespace对象的属性上, 可以通过Namespace对象属性访问, 例如args.path

非必须位置参数

上面的代码必须输入位置参数, 否则会报错

usage: ls [-h] path
ls: error: the following arguments are required: path

有时候, ls 命令不输入任何路径的话就表示列出当前目录的文件列表

import argparse

parser = argparse.ArgumentParser(prog='ls', add_help=True, description='list directory contents')  # 获取参数解析器
parser.add_argument('path', nargs='?', default='.', help='path help')   # 增加缺省值路径, 帮助

args = parser.parse_args()  # 分析参数, 同时传入可迭代参数
print(args, args.path)  # 打印名词空间收集的参数
parser.print_help()  # 打印帮助

运行结果

Namespace(path='.') .
usage: ls [-h] [path]

list directory contents

positional arguments:
  path        path help

optional arguments:
  -h, --help  show this help message and exit

可以看出在此处path 也变成可选的位置参数, 没有提供参数就使用缺省值"." 表示当前路径

  • help 表示帮助文档中这个参数的描述

  • nargs 表示这个参数接收结果参数

    • ? 表示可有可无
    • + 表示至少一个
    • * 可以是任意个
    • 数字表示必须是指定数目个
  • default 表示如果不提供该参数就使用这个值, 一般会配合 ? 、* 使用

选项参数

-l 的实现

  • parser.add_argument(’-l’) 就增加了选项参数, 参数定义为 ls [-h] [-l L] [path] , 和我们想要得到的有点出入, 我们期望的是 [-l] 不带 L选项的
  • nargs能解决吗 ? nargs=? 还是 nargs=0 ?

得到两次运行结果 :

Namespace(l='.', path='.') .
usage: ls [-h] [-l [L]] [path]

list directory contents

positional arguments:
  path        path help

optional arguments:
  -h, --help  show this help message and exit
  -l [L]      path help
raise ValueError("length of metavar tuple does not match nargs")
ValueError: length of metavar tuple does not match nargs
  • 根据运行结果得出nargs 是控制位置参数和选项参数的,并不能解决这里 -l 的问题, 为了解决这个问题引出了action 参数 …
  • parser.add_argument(’-l’, action=‘store_ture’)
import argparse

parser = argparse.ArgumentParser(prog='ls', add_help=True, description='list directory contents')  # 获取参数解析器
parser.add_argument('path', nargs='?', default='.', help='path help')   # 增加缺省值路径, 帮助
parser.add_argument('-l', action='store_true')

args = parser.parse_args()  # 分析参数, 同时传入可迭代参数
print(args)
parser.print_help()  # 打印帮助

运行结果

Namespace(l=False, path='.')
usage: ls [-h] [-l] [path]

list directory contents

positional arguments:
  path        path help

optional arguments:
  -h, --help  show this help message and exit
  -l
  • 这里已经解决了上面-l 选项传参的问题
  • ls -l 得到Namespace(l=True, path=’.’), 提供-l 值是True
  • ls得到Namespace(l=False, path=’.’), 提供-l 值是False
  • parser.add_argument(’-l’, action=‘store_const’, const=20), 表示如果提供-l 选项则对应的值是20, 如果不提供, 对应值是None

-a 的实现

  • parser.add_argument(’-a’, ‘–all’, action=‘store_ture’)
  • 长短选项可以同时给出, 与上面-l 类似

无参数传入运行

import argparse

parser = argparse.ArgumentParser(prog='ls', add_help=True, description='list directory contents')
parser.add_argument('path', nargs='?', default='.', help='dictory')
parser.add_argument('-l', action='store_true', help='use a long listing format')
parser.add_argument('-a', '--all', action='store_true', help='show all files, do not ignore entries starting with.')
args = parser.parse_args()
print(args)
parser.print_help()

运行结果

Namespace(all=False, l=False, path='.')
usage: ls [-h] [-l] [-a] [path]

list directory contents

positional arguments:
  path        dictory

optional arguments:
  -h, --help  show this help message and exit
  -l          use a long listing format
  -a, --all   show all files, do not ignore entries starting with.

带参传入运行

import argparse

parser = argparse.ArgumentParser(prog='ls', add_help=True, description='list directory contents')
parser.add_argument('path', nargs='?', default='.', help='dictory')
parser.add_argument('-l', action='store_true', help='use a long listing format')
parser.add_argument('-a', '--all', action='store_true', help='show all files, do not ignore entries starting with.')
args = parser.parse_args(('-l', '-a', '/tmp'))
print(args)
parser.print_help()

运行结果

Namespace(all=True, l=True, path='/tmp')
usage: ls [-h] [-l] [-a] [path]

list directory contents

positional arguments:
  path        dictory

optional arguments:
  -h, --help  show this help message and exit
  -l          use a long listing format
  -a, --all   show all files, do not ignore entries starting with.

ls 业务功能的实现

  • 到目前为止, 已经解决了参数的定义和传参的问题, 下面解决下面问题

    • 列出指定路径文件, 默认不递归
    • - a 显示所有文件, 包括隐藏文件
    • -l 详细列表模式显示

功能代码实现

def listdir(path, all=False):
    """列出本目录文件"""
    p = Path(path)
    # for f in p.iterdir():
    #     if not all and f.name.startswith('.'):
    #         continue
    #     yield f.name
    # yield from filter(lambda f : all or not f.name.startswith('.'), p.iterdir())
    yield from map(str, filter(lambda f : all or not f.name.startswith('.'), p.iterdir()))

def _getfiletype(f:Path):
    """获取文件类型"""
    if f.is_dir():
        return 'd'
    elif f.is_block_device():
        return 'b'
    elif f.is_char_device():
        return 'c'
    elif f.is_socket():
        return 's'
    elif f.is_symlink():
        return 'l'
    else:
        return '-'

def listdirdetail(path, all=False):
    """详细列出本目录"""
    p = Path(path)
    for f in p.iterdir():
        if not all and f.name.startswith('.'):
            continue
        stat = f.stat()
        t = _getfiletype(f)
        mode = oct(stat.st_mode)[-3:]
        mtime = datetime.fromtimestamp(stat.st_mtime).strftime('%Y %m %d %H:%M:%S')
        yield (t, mode, stat.st_nlink, stat.st_uid, stat.st_gid, stat.st_size, mtime, f.name)

modelist = dict(zip(range(9), ['r', 'w', 'x', 'r', 'w', 'x', 'r', 'w', 'x']))
# modelist = list('rwx'*3)
def _getmodestr(mode:int):
    # 二进制位移
    mstr = ''
    for i in range(8, -1, -1):
        mstr += modelist[8-i] if mode >> i & 1 else '-'
    # mstr = ''
    # for i, v in enumerate('{:09b}'.format(mode)[-9:]):
    #     mstr += modelist[i] if v == '1' else '-'
    return mstr

合并列出文件的两个函数
listdirdetail 和 listdir 几乎一样, 太多重复, 考虑合并

def listdir(path, all=False, detail=False):
    """合并后"""
    p = Path(path)
    for i in p.iterdir():
        if not all and i.name.startswith('.'):
            continue
        if not detail:
            yield (i.name,)
        else:
            stat = i.stat()
            mode = _getfiletype(i) + _getmodestr(stat.st_mode)
            mtime = datetime.fromtimestamp(stat.st_mtime).strftime('%Y %m %d %H:%M:%S')
            yield (mode, stat.st_nlink, stat.st_uid, stat.st_gid, stat.st_size, mtime, i.name)

排序
ls 的显示是把文件名按照升序排序输出

sorted(listdir(args.path, detail=True), key=lambda x: x[-1])

-h 的实现
-h , --human-readable, 如果-l 存在, -h 有效

  • 增加选项参数
parser.add_argument('-h', '--human-readable', action='store_true', help='with -l, print sizes in human readable format ')
  • 增加一个函数, 解决单位转换
def _gethuman(size:int):
    units = ' KMGTP'
    depth = 0
    while size > 1000 and depth + 1 < len(units):   # size大于1000且depth不是最后一个
        size = size // 1000    # 若按照1024处理, 整除1024即可
        depth += 1
    return "{}{}".format(size, units[depth])
  • 在-l 逻辑部分增加处理
size = stat.st_size if not human else _gethuman(stat.st_size)

改进mode 的方法

import stat
from pathlib import Path

stat.filemode(Path().stat().st_mode)

其他的完善
uid、gid 的转换

  • pwd 模块, Thepassword database, 提供访问Linux 、Unix 的password文件的方式, windows下没有.
  • pwd.getpwuid(Path().stat.st_uid).pw_name, grp模块, Linux、Unix 获取信息的模块, windows没有
  • grp.getgrgid(Path().stat().st_gid).gr_name, pathlib 模块, Path().group()也可以, 本质上它们就是调用pwd模块和grp模块
  • 由于windows 不支持这里不加入uid、gid的转换

最终代码

import stat
import argparse
from pathlib import Path
from datetime import datetime

parser = argparse.ArgumentParser(prog='ls', add_help=False, description='list directory contents')
parser.add_argument('path', nargs='?', default='.', help='dictory')
parser.add_argument('-l', action='store_true', help='use a long listing format')
parser.add_argument('-a', '--all', action='store_true', help='show all files, do not ignore entries starting with.')
parser.add_argument('-h', '--human-readable', action='store_true', help='with -l, print sizes in human readable format ',  dest='human')    # dest改变属性姓名

def listdir(path, all=False, detail=False, human=False):
    def _gethuman(size:int):
        units = ' KMGTP'
        depth = 0
        while size > 1000 and depth + 1 < len(units):
            size = size // 1000
            depth += 1
        return "{}{}".format(size, units[depth])
    def _listdir(path, all=False, detial=False, human=False):
        p = Path(path)
        for i in p.iterdir():
            if not all and i.name.startswith('.'):
                continue
            if not detail:
                yield (i.name,)
            else:
                st = i.stat()
                # mode = _getfiletype(i) + _getmodestr(stat.st_mode)
                mode = stat.filemode(st.st_mode)
                mtime = datetime.fromtimestamp(st.st_mtime).strftime('%Y %m %d %H:%M:%S')
                yield (mode, st.st_nlink, st.st_uid, st.st_gid, st.st_size, mtime, i.name)

    yield from sorted(_listdir(path, all, detail, human), key=lambda x: x[-1])

if __name__  == '__main__':
    args = parser.parse_args()
    print(args)
    parser.print_help()
    files = listdir(args.path, args.all, args.l, args.human)
    print(list(files))

运行结果

Namespace(all=True, human=True, l=True, path='.')
usage: ls [-l] [-a] [-h] [path]

list directory contents

positional arguments:
  path                  dictory

optional arguments:
  -l                    use a long listing format
  -a, --all             show all files, do not ignore entries starting with.
  -h, --human-readable  with -l, print sizes in human readable format
[('-rw-rw-rw-', 1, 0, 0, 2578, '2019 05 08 00:57:19', '*****.py'), ('-rw-rw-rw-', 1, 0, 0, 1804, '2019 05 06 15:20:51', '******.py'), ('-rw-rw-rw-', 1, 0, 0, 421, '2019 05 07 23:32:55', 'Python.py')]

测试

$ python xxx.py -lah

也可以在RUN -> Run / Debug Configurations -> Python
找到Paramters栏输入参数

你可能感兴趣的:(Python)