xstt 2026.4.23__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xstt-2026.4.23/PKG-INFO +11 -0
- xstt-2026.4.23/README.md +0 -0
- xstt-2026.4.23/book_mark.py +208 -0
- xstt-2026.4.23/disk_usage.py +305 -0
- xstt-2026.4.23/fast_cp.py +176 -0
- xstt-2026.4.23/ls.py +167 -0
- xstt-2026.4.23/pyproject.toml +38 -0
- xstt-2026.4.23/rm.py +131 -0
- xstt-2026.4.23/setup.cfg +4 -0
- xstt-2026.4.23/xstt.egg-info/PKG-INFO +11 -0
- xstt-2026.4.23/xstt.egg-info/SOURCES.txt +13 -0
- xstt-2026.4.23/xstt.egg-info/dependency_links.txt +1 -0
- xstt-2026.4.23/xstt.egg-info/entry_points.txt +7 -0
- xstt-2026.4.23/xstt.egg-info/requires.txt +5 -0
- xstt-2026.4.23/xstt.egg-info/top_level.txt +5 -0
xstt-2026.4.23/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xstt
|
|
3
|
+
Version: 2026.4.23
|
|
4
|
+
Summary: 一些终端实用命令行工具
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: python-pyper
|
|
8
|
+
Requires-Dist: rich
|
|
9
|
+
Requires-Dist: typer-slim[standard]
|
|
10
|
+
Requires-Dist: rapidfuzz
|
|
11
|
+
Requires-Dist: pypinyin
|
xstt-2026.4.23/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: xiash$
|
|
3
|
+
Date: 2025/10/19$
|
|
4
|
+
Project: terminal_tools$
|
|
5
|
+
Description: $
|
|
6
|
+
***
|
|
7
|
+
* _ooOoo_
|
|
8
|
+
* o8888888o
|
|
9
|
+
* 88" . "88
|
|
10
|
+
* (| -_- |)
|
|
11
|
+
* O\ = /O
|
|
12
|
+
* ___/`---'\____
|
|
13
|
+
* . ' \\| |// `.
|
|
14
|
+
* / \\||| : |||// \
|
|
15
|
+
* / _||||| -:- |||||- \
|
|
16
|
+
* | | \\\ - /// | |
|
|
17
|
+
* | \_| ''\---/'' | |
|
|
18
|
+
* \ .-\__ `-` ___/-. /
|
|
19
|
+
* ___`. .' /--.--\ `. . __
|
|
20
|
+
* ."" '< `.___\_<|>_/___.' >'"".
|
|
21
|
+
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
|
|
22
|
+
* \ \ `-. \_ __\ /__ _/ .-` / /
|
|
23
|
+
* ======`-.____`-.___\_____/___.-`____.-'======
|
|
24
|
+
* `=---='
|
|
25
|
+
* .............................................
|
|
26
|
+
* 佛曰:bug泛滥,我已瘫痪!
|
|
27
|
+
"""
|
|
28
|
+
import json
|
|
29
|
+
import typer
|
|
30
|
+
from pypinyin import lazy_pinyin, Style
|
|
31
|
+
from rapidfuzz import process, fuzz
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from rich import print
|
|
34
|
+
from typing import Iterable
|
|
35
|
+
|
|
36
|
+
CONFIG_PATH = Path.home() / '.bookmarks/config.ini'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_browser_bookmarks(bookmark_files_path: str | Path | Iterable[Path | str]) -> list[dict]:
|
|
40
|
+
if isinstance(bookmark_files_path, (Path, str)):
|
|
41
|
+
bookmark_files_path = [bookmark_files_path]
|
|
42
|
+
bookmarks = []
|
|
43
|
+
|
|
44
|
+
def traverse_nodes(node):
|
|
45
|
+
if node.get('type') == 'url':
|
|
46
|
+
bookmarks.append({
|
|
47
|
+
'title': node.get('name', ''),
|
|
48
|
+
'url': node.get('url', '')
|
|
49
|
+
})
|
|
50
|
+
elif node.get('type') == 'folder':
|
|
51
|
+
for child in node.get('children', []):
|
|
52
|
+
traverse_nodes(child)
|
|
53
|
+
|
|
54
|
+
for bookmark_file in bookmark_files_path:
|
|
55
|
+
bookmark_file = Path(str(bookmark_file).strip())
|
|
56
|
+
if not bookmark_file.exists():
|
|
57
|
+
continue
|
|
58
|
+
with open(bookmark_file.as_posix().strip(), 'r', encoding='utf-8') as f:
|
|
59
|
+
data = json.load(f)
|
|
60
|
+
|
|
61
|
+
# 遍历所有根节点 (书签栏, 其他书签等)
|
|
62
|
+
for root_key in data.get('roots', {}):
|
|
63
|
+
traverse_nodes(data['roots'][root_key])
|
|
64
|
+
|
|
65
|
+
return bookmarks
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_search_keys(text: str) -> list[str]:
|
|
69
|
+
"""
|
|
70
|
+
为一段文本(例如书签标题)生成所有可能的搜索键。
|
|
71
|
+
"""
|
|
72
|
+
if not text:
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
# 确保文本是字符串,pypinyin 处理非中文字符时会原样返回
|
|
76
|
+
text_lower = str(text).lower()
|
|
77
|
+
|
|
78
|
+
# 1. 原始关键字 (小写)
|
|
79
|
+
keys = [text_lower]
|
|
80
|
+
|
|
81
|
+
# 2. 全拼音 (例如: "zhongguoyinhang")
|
|
82
|
+
# style=Style.NORMAL: 普通拼音,不带声调
|
|
83
|
+
# errors='ignore': 忽略无法转换的字符(如特殊符号)
|
|
84
|
+
pinyin_full = "".join(lazy_pinyin(text, style=Style.NORMAL, errors='ignore'))
|
|
85
|
+
if pinyin_full != text_lower:
|
|
86
|
+
keys.append(pinyin_full)
|
|
87
|
+
|
|
88
|
+
# 3. 拼音首字母 (例如: "zgyh")
|
|
89
|
+
# style=Style.FIRST_LETTER: 拼音首字母
|
|
90
|
+
pinyin_initials = "".join(lazy_pinyin(text, style=Style.FIRST_LETTER, errors='ignore'))
|
|
91
|
+
if pinyin_initials != text_lower:
|
|
92
|
+
keys.append(pinyin_initials)
|
|
93
|
+
|
|
94
|
+
return keys
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_search_index(bookmarks: list[dict]):
|
|
98
|
+
"""
|
|
99
|
+
构建用于 rapidfuzz 搜索的扁平化索引。
|
|
100
|
+
|
|
101
|
+
返回:
|
|
102
|
+
- search_choices (list[str]): 所有可能的搜索键的扁平列表
|
|
103
|
+
- index_map (dict[int, int]): search_choices的索引 -> bookmarks列表的索引
|
|
104
|
+
"""
|
|
105
|
+
search_choices = []
|
|
106
|
+
index_map = {} # key: 索引 in search_choices, value: 索引 in bookmarks
|
|
107
|
+
|
|
108
|
+
for bookmark_index, bookmark in enumerate(bookmarks):
|
|
109
|
+
# 我们主要搜索标题,也可以加入URL
|
|
110
|
+
title = bookmark.get('title', '')
|
|
111
|
+
url = bookmark.get('url', '')
|
|
112
|
+
|
|
113
|
+
# 为标题生成搜索键
|
|
114
|
+
title_keys = get_search_keys(title)
|
|
115
|
+
|
|
116
|
+
# (可选) 为URL的域名部分生成搜索键
|
|
117
|
+
# url_keys = get_search_keys(url.split('/')[2] if '/' in url else url)
|
|
118
|
+
# all_keys = title_keys + url_keys
|
|
119
|
+
|
|
120
|
+
all_keys = title_keys
|
|
121
|
+
|
|
122
|
+
for key in all_keys:
|
|
123
|
+
if key: # 确保键不为空
|
|
124
|
+
search_choices.append(key)
|
|
125
|
+
index_map[len(search_choices) - 1] = bookmark_index
|
|
126
|
+
|
|
127
|
+
return search_choices, index_map
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def search_bookmarks(
|
|
131
|
+
query: str,
|
|
132
|
+
bookmarks: list[dict],
|
|
133
|
+
search_choices: list[str],
|
|
134
|
+
index_map: dict[int, int],
|
|
135
|
+
limit: int = 10,
|
|
136
|
+
score_threshold: int = 50
|
|
137
|
+
):
|
|
138
|
+
"""
|
|
139
|
+
使用 rapidfuzz 进行模糊搜索
|
|
140
|
+
"""
|
|
141
|
+
query_lower = query.lower()
|
|
142
|
+
|
|
143
|
+
# rapidfuzz.process.extract() 是核心!
|
|
144
|
+
# 它会返回一个列表: [(匹配到的键, 匹配分数, 在search_choices中的索引), ...]
|
|
145
|
+
# 我们选择一个适合路径和缩写的计分器,如 WPath
|
|
146
|
+
# 我们多搜索一些结果 (limit * 3),因为多个键可能指向同一个书签
|
|
147
|
+
results = process.extract(
|
|
148
|
+
query_lower,
|
|
149
|
+
search_choices,
|
|
150
|
+
scorer=fuzz.WRatio, # WPath 计分器对缩写和顺序错乱有较好支持
|
|
151
|
+
limit=limit
|
|
152
|
+
)
|
|
153
|
+
for (matched_key, score, key_index) in results:
|
|
154
|
+
# 1. 通过映射表找到原始书签的索引
|
|
155
|
+
bookmark_index = index_map[key_index]
|
|
156
|
+
|
|
157
|
+
# 2. 检查是否已经添加过这个书签(避免重复)
|
|
158
|
+
if score > score_threshold:
|
|
159
|
+
# 3. 获取原始书签
|
|
160
|
+
bookmark = bookmarks[bookmark_index]
|
|
161
|
+
yield f'{bookmark["title"]}:{bookmark["url"]}'
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def bookmarks_search(
|
|
165
|
+
query: str = typer.Argument('', help="书签查询关键字"),
|
|
166
|
+
import_bookmarks_file: Path = typer.Option(None, '-i', '--import', help="书签文件路径"),
|
|
167
|
+
limit: int = typer.Option(10, '-l', '--limit', min=1, help='限制查询得数量'),
|
|
168
|
+
score: int = typer.Option(50, '-s', '--score', min=0, max=99, help='分数阈值,[0,99]之间')
|
|
169
|
+
):
|
|
170
|
+
"""
|
|
171
|
+
浏览器书签搜索
|
|
172
|
+
"""
|
|
173
|
+
default_bookmarks_file = Path.home() / r'AppData/Local/Microsoft/Edge/User Data/Default/Bookmarks'
|
|
174
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
f = open(CONFIG_PATH, 'a+')
|
|
176
|
+
f.seek(0)
|
|
177
|
+
bookmarks_files = set(line.strip() for line in f if line.strip())
|
|
178
|
+
if not bookmarks_files:
|
|
179
|
+
if default_bookmarks_file.exists():
|
|
180
|
+
bookmarks_files.add(default_bookmarks_file)
|
|
181
|
+
f.write(default_bookmarks_file.as_posix() + '\n')
|
|
182
|
+
else:
|
|
183
|
+
print(f'[yellow]{default_bookmarks_file} 不存在,请通过 --import 参数手动导入浏览器书签文件路径!')
|
|
184
|
+
|
|
185
|
+
if import_bookmarks_file:
|
|
186
|
+
if import_bookmarks_file.as_posix() in bookmarks_files:
|
|
187
|
+
print(f'[yellow]{import_bookmarks_file} 已配置过,无需重复导入!')
|
|
188
|
+
elif import_bookmarks_file.exists():
|
|
189
|
+
bookmarks_files.add(import_bookmarks_file)
|
|
190
|
+
f.write(import_bookmarks_file.as_posix() + '\n')
|
|
191
|
+
print(f'[green]{import_bookmarks_file} 导入成功!')
|
|
192
|
+
else:
|
|
193
|
+
print(f'[yellow]{import_bookmarks_file} 不存在!')
|
|
194
|
+
f.close()
|
|
195
|
+
if query:
|
|
196
|
+
bookmarks = parse_browser_bookmarks(bookmarks_files)
|
|
197
|
+
choices, mapping = build_search_index(bookmarks)
|
|
198
|
+
results = search_bookmarks(query, bookmarks, choices, mapping, limit, score)
|
|
199
|
+
for result in results:
|
|
200
|
+
print(result)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main():
|
|
204
|
+
typer.run(bookmarks_search)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == '__main__':
|
|
208
|
+
main()
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: xiash$
|
|
3
|
+
Date: 2025/5/11$
|
|
4
|
+
Project: terminal_tools$
|
|
5
|
+
Description: $
|
|
6
|
+
***
|
|
7
|
+
* _ooOoo_
|
|
8
|
+
* o8888888o
|
|
9
|
+
* 88" . "88
|
|
10
|
+
* (| -_- |)
|
|
11
|
+
* O\ = /O
|
|
12
|
+
* ___/`---'\____
|
|
13
|
+
* . ' \\| |// `.
|
|
14
|
+
* / \\||| : |||// \
|
|
15
|
+
* / _||||| -:- |||||- \
|
|
16
|
+
* | | \\\ - /// | |
|
|
17
|
+
* | \_| ''\---/'' | |
|
|
18
|
+
* \ .-\__ `-` ___/-. /
|
|
19
|
+
* ___`. .' /--.--\ `. . __
|
|
20
|
+
* ."" '< `.___\_<|>_/___.' >'"".
|
|
21
|
+
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
|
|
22
|
+
* \ \ `-. \_ __\ /__ _/ .-` / /
|
|
23
|
+
* ======`-.____`-.___\_____/___.-`____.-'======
|
|
24
|
+
* `=---='
|
|
25
|
+
* .............................................
|
|
26
|
+
* 佛曰:bug泛滥,我已瘫痪!
|
|
27
|
+
"""
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import os.path as osp
|
|
32
|
+
import platform
|
|
33
|
+
from copy import deepcopy
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from pyper import task
|
|
36
|
+
from typing import Tuple, Iterable
|
|
37
|
+
from rich import print
|
|
38
|
+
from rich.text import Text
|
|
39
|
+
from rich.progress import Progress, TimeElapsedColumn, MofNCompleteColumn
|
|
40
|
+
|
|
41
|
+
cache_dir = os.environ.get('DISK_USAGE_CACHE_DIR', Path.home() / '.disk_usage')
|
|
42
|
+
disk_usage_cache = {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def total_st_size(path: Path | str) -> Tuple[int, Path]:
|
|
46
|
+
"""
|
|
47
|
+
获取文件或目录(包括所有子目录及文件)大小
|
|
48
|
+
:param path:
|
|
49
|
+
:return:
|
|
50
|
+
"""
|
|
51
|
+
path = Path(path)
|
|
52
|
+
if path.is_file():
|
|
53
|
+
return file_st_size(path), path
|
|
54
|
+
elif path.is_symlink():
|
|
55
|
+
return 0, Path(f"{path}(-> {path.readlink()})")
|
|
56
|
+
elif path.is_dir():
|
|
57
|
+
st_mtime = path.stat().st_mtime
|
|
58
|
+
key_path = osp.abspath(path)
|
|
59
|
+
cache_info = disk_usage_cache.get(key_path)
|
|
60
|
+
if cache_info and st_mtime == cache_info[1]:
|
|
61
|
+
size = cache_info[0]
|
|
62
|
+
else:
|
|
63
|
+
# size = sum(st_size(file) for file in path.iter_dir(recursive=True))
|
|
64
|
+
try:
|
|
65
|
+
# size = dir_total_size(path)
|
|
66
|
+
size = dir_total_size_parallel(path)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
# print(f"[red]警告:无法访问 {path},错误:{e}")
|
|
69
|
+
size = 0
|
|
70
|
+
disk_usage_cache[key_path] = (size, st_mtime)
|
|
71
|
+
return size, path
|
|
72
|
+
return 0, path
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def dir_total_size_parallel(src: str | Path) -> int:
|
|
76
|
+
"""
|
|
77
|
+
获取目录下所有文件的大小总和,一级子目录并行进行统计
|
|
78
|
+
:param src:
|
|
79
|
+
:return:
|
|
80
|
+
"""
|
|
81
|
+
src = Path(src)
|
|
82
|
+
total_num = sum(file_st_size(file) for file in src.iterdir() if file.is_file())
|
|
83
|
+
workers = min(4, sum(1 for file in os.scandir(src) if file.is_dir()))
|
|
84
|
+
if workers == 0:
|
|
85
|
+
return total_num
|
|
86
|
+
pipeline = (
|
|
87
|
+
task(lambda path: (file for file in path.iterdir() if file.is_dir()), branch=True) |
|
|
88
|
+
task(dir_total_size, workers=workers, multiprocess=True)
|
|
89
|
+
)
|
|
90
|
+
for num in pipeline(src):
|
|
91
|
+
total_num += num
|
|
92
|
+
return total_num
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def dir_total_size(src_dir):
|
|
96
|
+
src_dir = Path(src_dir)
|
|
97
|
+
if src_dir.is_symlink():
|
|
98
|
+
return 0
|
|
99
|
+
st_mtime = src_dir.stat().st_mtime
|
|
100
|
+
key_path = osp.abspath(src_dir)
|
|
101
|
+
cache_info = disk_usage_cache.get(key_path)
|
|
102
|
+
if cache_info and st_mtime == cache_info[1]:
|
|
103
|
+
return cache_info[0]
|
|
104
|
+
return sum(
|
|
105
|
+
sum(map(lambda file: file_st_size(osp.join(walk_out[0], file)), walk_out[-1])) for walk_out in os.walk(src_dir)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def file_st_size(path: Path | str):
|
|
110
|
+
path = Path(path)
|
|
111
|
+
if path.is_symlink():
|
|
112
|
+
return 0
|
|
113
|
+
try:
|
|
114
|
+
return path.stat().st_size
|
|
115
|
+
except Exception:
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def convert_bytes(size_in_bytes) -> str:
|
|
120
|
+
"""
|
|
121
|
+
将字节数转换为易读的单位(KB, MB, GB)
|
|
122
|
+
:param size_in_bytes: 字节数
|
|
123
|
+
:return: 转换后的易读大小
|
|
124
|
+
"""
|
|
125
|
+
units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
126
|
+
index = 0
|
|
127
|
+
while size_in_bytes >= 1024 and index < len(units) - 1:
|
|
128
|
+
size_in_bytes /= 1024.0
|
|
129
|
+
index += 1
|
|
130
|
+
return f"{size_in_bytes:.2f} {units[index]}"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_disk_usage_in_one_depth(path: str | Path) -> Iterable[Tuple[int, Path]]:
|
|
134
|
+
"""
|
|
135
|
+
获取目标目录下的所有文件和子目录(深度为1)的大小
|
|
136
|
+
:param path:
|
|
137
|
+
:return:
|
|
138
|
+
"""
|
|
139
|
+
path = Path(path)
|
|
140
|
+
num_tasks = sum(1 for _ in os.scandir(path))
|
|
141
|
+
if not num_tasks:
|
|
142
|
+
yield 0, path
|
|
143
|
+
return
|
|
144
|
+
workers = min(4, num_tasks)
|
|
145
|
+
pipeline = (
|
|
146
|
+
task(path.iterdir, branch=True) |
|
|
147
|
+
task(total_st_size, workers=workers)
|
|
148
|
+
)
|
|
149
|
+
total_size = 0
|
|
150
|
+
for size, sub_path in pipeline():
|
|
151
|
+
total_size += size
|
|
152
|
+
if not path.is_absolute() and not str(path).startswith('.'):
|
|
153
|
+
sub_path = osp.join('.', sub_path)
|
|
154
|
+
yield size, sub_path
|
|
155
|
+
disk_usage_cache[osp.abspath(path)] = [total_size, osp.getmtime(path)]
|
|
156
|
+
yield total_size, path
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_disk_usage_all(path: str | Path) -> Iterable[Tuple[int, str]]:
|
|
160
|
+
"""
|
|
161
|
+
获取当前文件或目录及其所有子目录或者文件大小
|
|
162
|
+
:param path:
|
|
163
|
+
:return:
|
|
164
|
+
"""
|
|
165
|
+
pipeline = (
|
|
166
|
+
task(os.walk, branch=True) |
|
|
167
|
+
task(_walk_step, workers=4)
|
|
168
|
+
)
|
|
169
|
+
for out in pipeline(path, topdown=False):
|
|
170
|
+
yield from out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _walk_step(args):
|
|
174
|
+
root, dirs, files = args
|
|
175
|
+
root_st_size = 0
|
|
176
|
+
for file in files:
|
|
177
|
+
file_path = osp.join(root, file)
|
|
178
|
+
file_size = file_st_size(str(file_path))
|
|
179
|
+
root_st_size += file_size
|
|
180
|
+
yield file_size, file_path
|
|
181
|
+
for _dir in dirs:
|
|
182
|
+
dir_path = osp.join(root, _dir)
|
|
183
|
+
dir_st_size = total_st_size(str(dir_path))[0]
|
|
184
|
+
root_st_size += dir_st_size
|
|
185
|
+
disk_usage_cache[osp.abspath(root)] = [root_st_size, osp.getmtime(root)]
|
|
186
|
+
yield root_st_size, root
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def format_show_info_rich(size: int, path: str | Path) -> Text:
|
|
190
|
+
"""
|
|
191
|
+
格式化显示信息,rich库
|
|
192
|
+
:param size:
|
|
193
|
+
:param path:
|
|
194
|
+
:return:
|
|
195
|
+
"""
|
|
196
|
+
format_size = convert_bytes(size)
|
|
197
|
+
map_color = {
|
|
198
|
+
'B': 'white',
|
|
199
|
+
'KB': 'white',
|
|
200
|
+
'MB': 'green',
|
|
201
|
+
'GB': 'yellow',
|
|
202
|
+
'TB': 'red',
|
|
203
|
+
}
|
|
204
|
+
text = Text(f"{format_size:<10}{' ' * 10}{path}", style=map_color[format_size.split()[1]])
|
|
205
|
+
return text
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def update_disk_usage_cache_file(disk_usage_cache_file_path: str | Path, cache: dict):
|
|
209
|
+
Path(disk_usage_cache_file_path).parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
with open(disk_usage_cache_file_path, 'w', encoding='utf-8') as f:
|
|
211
|
+
json.dump(cache, f, indent=4, ensure_ascii=False)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def show_disk_usage(path: str, recursive: bool = False):
|
|
215
|
+
"""
|
|
216
|
+
获取当前文件或目录及其所有子目录或者文件大小
|
|
217
|
+
:param path:
|
|
218
|
+
:param recursive: 是否递归查询子目录
|
|
219
|
+
:return:
|
|
220
|
+
"""
|
|
221
|
+
path = Path(path.removesuffix('"'))
|
|
222
|
+
if not path.exists():
|
|
223
|
+
raise FileNotFoundError(f"{path} does not exist")
|
|
224
|
+
if path.is_file():
|
|
225
|
+
print(format_show_info_rich(file_st_size(path), path))
|
|
226
|
+
return
|
|
227
|
+
global disk_usage_cache
|
|
228
|
+
disk_usage_cache_file_path = get_disk_usage_cache_file(path)
|
|
229
|
+
disk_usage_cache = get_disk_usage_cache(disk_usage_cache_file_path)
|
|
230
|
+
if not recursive:
|
|
231
|
+
results = get_disk_usage_in_one_depth(path)
|
|
232
|
+
else:
|
|
233
|
+
results = get_disk_usage_all(path)
|
|
234
|
+
process = Progress(*Progress.get_default_columns(), TimeElapsedColumn())
|
|
235
|
+
pb = process.add_task(description="[green]正在统计文件及目录...", total=None)
|
|
236
|
+
process.start()
|
|
237
|
+
for sub_size, sub_path in results:
|
|
238
|
+
if not osp.isabs(sub_path) and not str(sub_path).startswith('.'):
|
|
239
|
+
sub_path = osp.join('.', sub_path)
|
|
240
|
+
print(format_show_info_rich(sub_size, sub_path))
|
|
241
|
+
process.update(pb, description='[purple]已统计完成!', )
|
|
242
|
+
process.stop()
|
|
243
|
+
update_disk_usage_cache_file(disk_usage_cache_file_path, disk_usage_cache)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_disk_usage_cache_file(path: str | Path) -> Path:
|
|
247
|
+
path = osp.abspath(path)
|
|
248
|
+
if platform.system() == 'Windows':
|
|
249
|
+
disk_usage_cache_file_path = cache_dir / f'{osp.splitdrive(path)[0].strip(":")}.json'
|
|
250
|
+
else:
|
|
251
|
+
disk_usage_cache_file_path = cache_dir / f'{path.split("/")[1]}.json'
|
|
252
|
+
return disk_usage_cache_file_path
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_disk_usage_cache(disk_usage_cache_file_path: str | Path) -> dict:
|
|
256
|
+
if not osp.exists(disk_usage_cache_file_path):
|
|
257
|
+
return {}
|
|
258
|
+
try:
|
|
259
|
+
with open(disk_usage_cache_file_path, 'r', encoding='utf-8') as f:
|
|
260
|
+
return json.load(f)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
print(f"[yellow]警告:无法读取缓存文件 {disk_usage_cache_file_path},错误:{e}")
|
|
263
|
+
return {}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def clear_unused_cache(path):
|
|
267
|
+
global disk_usage_cache
|
|
268
|
+
disk_usage_cache_file_path = get_disk_usage_cache_file(path)
|
|
269
|
+
disk_usage_cache = get_disk_usage_cache(disk_usage_cache_file_path)
|
|
270
|
+
if not disk_usage_cache:
|
|
271
|
+
print("[yellow]未发现缓存!")
|
|
272
|
+
return
|
|
273
|
+
new_cache = deepcopy(disk_usage_cache)
|
|
274
|
+
progress = Progress(*Progress.get_default_columns(), MofNCompleteColumn(), TimeElapsedColumn())
|
|
275
|
+
pb = progress.add_task(description="[green]正在清除无效缓存...", total=len(disk_usage_cache))
|
|
276
|
+
progress.start()
|
|
277
|
+
pipeline = (
|
|
278
|
+
task(disk_usage_cache.keys, branch=True) |
|
|
279
|
+
task(lambda key: (key, osp.exists(key)), workers=os.cpu_count())
|
|
280
|
+
)
|
|
281
|
+
for cache_key, is_valid in pipeline():
|
|
282
|
+
if not is_valid:
|
|
283
|
+
del new_cache[cache_key]
|
|
284
|
+
print(f"[green]已清除无效缓存:{cache_key}")
|
|
285
|
+
progress.advance(pb)
|
|
286
|
+
update_disk_usage_cache_file(disk_usage_cache_file_path, new_cache)
|
|
287
|
+
progress.update(pb, description='[purple]已清除完成!')
|
|
288
|
+
progress.stop()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def main():
|
|
292
|
+
parser = argparse.ArgumentParser()
|
|
293
|
+
parser.add_argument('path', type=str, nargs='?', default='.', help='要查询的文件或目录路径')
|
|
294
|
+
parser.add_argument('-a', '--all', action='store_true', help='递归查询所有子目录')
|
|
295
|
+
parser.add_argument('-c', '--clear-cache', action='store_true', help='清除无效缓存')
|
|
296
|
+
args = parser.parse_args()
|
|
297
|
+
if args.clear_cache:
|
|
298
|
+
clear_unused_cache(args.path)
|
|
299
|
+
|
|
300
|
+
else:
|
|
301
|
+
show_disk_usage(args.path, args.all)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == '__main__':
|
|
305
|
+
main()
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# @Time: 2025/5/9 上午10:17
|
|
2
|
+
# @Autor: XS
|
|
3
|
+
# @File: fast_cp.py
|
|
4
|
+
# @Software: PyCharm
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
/**
|
|
8
|
+
* _ooOoo_
|
|
9
|
+
* o8888888o
|
|
10
|
+
* 88" . "88
|
|
11
|
+
* (| -_- |)
|
|
12
|
+
* O\ = /O
|
|
13
|
+
* ___/`---'\____
|
|
14
|
+
* . ' \\| |// `.
|
|
15
|
+
* / \\||| : |||// \
|
|
16
|
+
* / _||||| -:- |||||- \
|
|
17
|
+
* | | \\\ - /// | |
|
|
18
|
+
* | \_| ''\---/'' | |
|
|
19
|
+
* \ .-\__ `-` ___/-. /
|
|
20
|
+
* ___`. .' /--.--\ `. . __
|
|
21
|
+
* ."" '< `.___\_<|>_/___.' >'"".
|
|
22
|
+
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
|
|
23
|
+
* \ \ `-. \_ __\ /__ _/ .-` / /
|
|
24
|
+
* ======`-.____`-.___\_____/___.-`____.-'======
|
|
25
|
+
* `=---=' bug泛滥 佛已瘫痪
|
|
26
|
+
"""
|
|
27
|
+
import argparse
|
|
28
|
+
import os
|
|
29
|
+
import os.path as osp
|
|
30
|
+
from shutil import copymode
|
|
31
|
+
from pyper import task
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from rich.progress import Progress, TimeElapsedColumn, TransferSpeedColumn, DownloadColumn
|
|
34
|
+
from rich import print
|
|
35
|
+
|
|
36
|
+
COPY_BUFSIZE = 1024 * 1024 if os.name == 'nt' else 64 * 1024
|
|
37
|
+
progress = Progress(*Progress.get_default_columns(), TransferSpeedColumn(), DownloadColumn(), TimeElapsedColumn())
|
|
38
|
+
task_id = progress.add_task('[green]文件大小统计中...', total=None)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# from typing import Iterable
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# def get_all_files(src: (str, MyPath)) -> Iterable[MyPath]:
|
|
45
|
+
# src = MyPath(src)
|
|
46
|
+
# if not src.exists():
|
|
47
|
+
# raise FileNotFoundError(f'{src}不存在')
|
|
48
|
+
# return filter(lambda file: file.is_file(), src.iter_dir(recursive=True))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_file_in_snapshot(file: Path, snapshot_time: float):
|
|
52
|
+
try:
|
|
53
|
+
return file.stat().st_mtime <= snapshot_time
|
|
54
|
+
except Exception:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _copy(src: str | Path, dst: str | Path) -> str:
|
|
59
|
+
src, dst = Path(src), Path(dst)
|
|
60
|
+
if dst.is_dir():
|
|
61
|
+
dst = dst / src.name
|
|
62
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
if the_same_file_exists(src, dst):
|
|
64
|
+
progress.advance(task_id, src.stat().st_size)
|
|
65
|
+
return f'[yellow]{src}已存在,直接跳过![/yellow]'
|
|
66
|
+
with open(src, 'rb') as src_file:
|
|
67
|
+
with open(dst, 'wb') as dst_file:
|
|
68
|
+
while True:
|
|
69
|
+
data = src_file.read(COPY_BUFSIZE)
|
|
70
|
+
if not data:
|
|
71
|
+
break
|
|
72
|
+
dst_file.write(data)
|
|
73
|
+
progress.advance(task_id, len(data))
|
|
74
|
+
copymode(src, dst)
|
|
75
|
+
return dst.as_posix()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _copy_file(file_path: Path, src_dir: str, dst_dir: str) -> str:
|
|
79
|
+
dst_path = Path(str(file_path).replace(src_dir, dst_dir))
|
|
80
|
+
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
try:
|
|
82
|
+
return _copy(file_path, dst_path)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
return f'[red]{file_path}拷贝失败,错误信息为:{e}'
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def the_same_file_exists(src_file: str | Path, dst_file: str | Path) -> bool:
|
|
88
|
+
src_file, dst_file = Path(src_file), Path(dst_file)
|
|
89
|
+
return dst_file.exists() and src_file.stat().st_size == dst_file.stat().st_size
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def fast_copy(src: str, dst: str, workers: int = 20, verbose: bool = False) -> None:
|
|
93
|
+
src, dst = Path(src).absolute(), Path(dst).absolute()
|
|
94
|
+
if not src.exists():
|
|
95
|
+
raise FileNotFoundError(f'{src}不存在!')
|
|
96
|
+
|
|
97
|
+
progress.start()
|
|
98
|
+
desc = f'[green]{src} [yellow]--->[/yellow] {dst}'
|
|
99
|
+
if dst.is_dir() and src.name != dst.name:
|
|
100
|
+
dst = dst / src.name
|
|
101
|
+
if src.is_file():
|
|
102
|
+
total = src.stat().st_size
|
|
103
|
+
progress.update(task_id, total=total, description=desc)
|
|
104
|
+
out = _copy(src, dst)
|
|
105
|
+
verbose and print(out)
|
|
106
|
+
|
|
107
|
+
else:
|
|
108
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
total = get_dir_total_size_parallel(src)
|
|
110
|
+
# snapshot_time = time.time()
|
|
111
|
+
# total = sum(
|
|
112
|
+
# sum(map(lambda file: osp.getsize(osp.join(walk_out[0], file)), walk_out[-1])) for walk_out in os.walk(src)
|
|
113
|
+
# )
|
|
114
|
+
progress.update(task_id, total=total, description=desc)
|
|
115
|
+
pipeline = (
|
|
116
|
+
task(lambda path: (file for file in path.rglob('*') if file.is_file()), branch=True) |
|
|
117
|
+
task(_copy_file, bind=task.bind(src_dir=str(src), dst_dir=str(dst)), workers=workers)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
for out in pipeline(src):
|
|
121
|
+
out = str(out)
|
|
122
|
+
verbose and print(out)
|
|
123
|
+
'错误' in out and print(out)
|
|
124
|
+
progress.update(task_id, completed=total)
|
|
125
|
+
progress.stop()
|
|
126
|
+
print(f'[green]拷贝完成![/green]'.center(100, '*'))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_file_size(src_file: str | Path) -> int:
|
|
130
|
+
try:
|
|
131
|
+
file_size = osp.getsize(src_file)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
# print(f'[yellow]{src_file} 获取文件大小失败,错误信息为:{e}')
|
|
134
|
+
file_size = 0
|
|
135
|
+
return file_size
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_dir_total_size(src_dir):
|
|
139
|
+
return sum(
|
|
140
|
+
sum(map(lambda file: get_file_size(str(osp.join(walk_out[0], file))), walk_out[-1])) for walk_out in
|
|
141
|
+
os.walk(src_dir)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_dir_total_size_parallel(src: str | Path) -> int:
|
|
146
|
+
"""
|
|
147
|
+
获取目录下所有文件的大小总和,一级子目录并行进行统计
|
|
148
|
+
:param src:
|
|
149
|
+
:return:
|
|
150
|
+
"""
|
|
151
|
+
src = Path(src)
|
|
152
|
+
total_num = sum(get_file_size(file) for file in src.iterdir() if file.is_file())
|
|
153
|
+
workers = min(4, sum(1 for file in os.scandir(src) if file.is_dir()))
|
|
154
|
+
if workers == 0:
|
|
155
|
+
return total_num
|
|
156
|
+
pipeline = (
|
|
157
|
+
task(lambda path: (file for file in path.iterdir() if file.is_dir()), branch=True) |
|
|
158
|
+
task(get_dir_total_size, workers=workers, multiprocess=True)
|
|
159
|
+
)
|
|
160
|
+
for num in pipeline(src):
|
|
161
|
+
total_num += num
|
|
162
|
+
return total_num
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def main():
|
|
166
|
+
parser = argparse.ArgumentParser()
|
|
167
|
+
parser.add_argument('src', type=str, help='源路径')
|
|
168
|
+
parser.add_argument('dst', type=str, help='目标路径')
|
|
169
|
+
parser.add_argument('-t', '--threads', type=int, default=4, help='线程数,默认值为2倍cpu核心数')
|
|
170
|
+
parser.add_argument('-v', '--verbose', action='store_true', help='显示详细拷贝信息')
|
|
171
|
+
args = parser.parse_args()
|
|
172
|
+
fast_copy(args.src, args.dst, args.threads, args.verbose)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == '__main__':
|
|
176
|
+
main()
|
xstt-2026.4.23/ls.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: xiash$
|
|
3
|
+
Date: 2025/5/16$
|
|
4
|
+
Project: terminal_tools$
|
|
5
|
+
Description: $
|
|
6
|
+
***
|
|
7
|
+
* _ooOoo_
|
|
8
|
+
* o8888888o
|
|
9
|
+
* 88" . "88
|
|
10
|
+
* (| -_- |)
|
|
11
|
+
* O\ = /O
|
|
12
|
+
* ___/`---'\____
|
|
13
|
+
* . ' \\| |// `.
|
|
14
|
+
* / \\||| : |||// \
|
|
15
|
+
* / _||||| -:- |||||- \
|
|
16
|
+
* | | \\\ - /// | |
|
|
17
|
+
* | \_| ''\---/'' | |
|
|
18
|
+
* \ .-\__ `-` ___/-. /
|
|
19
|
+
* ___`. .' /--.--\ `. . __
|
|
20
|
+
* ."" '< `.___\_<|>_/___.' >'"".
|
|
21
|
+
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
|
|
22
|
+
* \ \ `-. \_ __\ /__ _/ .-` / /
|
|
23
|
+
* ======`-.____`-.___\_____/___.-`____.-'======
|
|
24
|
+
* `=---='
|
|
25
|
+
* .............................................
|
|
26
|
+
* 佛曰:bug泛滥,我已瘫痪!
|
|
27
|
+
"""
|
|
28
|
+
import argparse
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
from rich.console import Console
|
|
32
|
+
from rich.table import Table
|
|
33
|
+
from rich.panel import Panel
|
|
34
|
+
from rich.tree import Tree
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from disk_usage import convert_bytes
|
|
38
|
+
|
|
39
|
+
console = Console()
|
|
40
|
+
executable_extensions = {".exe", ".bat", ".cmd", ".ps1", ".msi", ".com", '.vbs'}
|
|
41
|
+
archive_files = ['.zip', '.rar', '.tar.gz', '.tar.bz2', '.7z', '.tar', '.gz', '.gzip', '.bz2', '.xz', '.iso',
|
|
42
|
+
'.cab', 'jar', '.war', '.ear']
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_permissions(path: str | Path) -> str:
|
|
46
|
+
path = Path(path)
|
|
47
|
+
mode = path.lstat().st_mode # 获取权限位
|
|
48
|
+
perm_list = []
|
|
49
|
+
map_who = {
|
|
50
|
+
'所有者': mode >> 6 & 0o7,
|
|
51
|
+
'组': mode >> 3 & 0o7,
|
|
52
|
+
'其他': mode & 0o7
|
|
53
|
+
}
|
|
54
|
+
# 解析权限
|
|
55
|
+
for name, who in map_who.items():
|
|
56
|
+
temp = []
|
|
57
|
+
if who & 0o4:
|
|
58
|
+
temp.append('读')
|
|
59
|
+
if who & 0o2:
|
|
60
|
+
temp.append('写')
|
|
61
|
+
if who & 0o1:
|
|
62
|
+
temp.append('执行')
|
|
63
|
+
perm_list.append(f"{name}: {','.join(temp)}")
|
|
64
|
+
|
|
65
|
+
return '\n'.join(perm_list)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def list_directory():
|
|
69
|
+
"""
|
|
70
|
+
列出目录下的所有文件及子目录信息(类似ls命令)
|
|
71
|
+
"""
|
|
72
|
+
args = parse_args()
|
|
73
|
+
target_dir = args.target_dir
|
|
74
|
+
target_dir = Path(target_dir)
|
|
75
|
+
abs_path = str(target_dir.resolve())
|
|
76
|
+
icon_map = {
|
|
77
|
+
'文件': '📄',
|
|
78
|
+
'目录': '📁',
|
|
79
|
+
'符号链接': '🔗',
|
|
80
|
+
'归档文件(压缩包)': '📦',
|
|
81
|
+
'可执行文件': '💻',
|
|
82
|
+
'python脚本': '🐍',
|
|
83
|
+
}
|
|
84
|
+
tree = Tree(
|
|
85
|
+
f"{icon_map['目录']} [cyan]{abs_path}",
|
|
86
|
+
guide_style="bold",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for file in target_dir.iterdir():
|
|
90
|
+
if file.is_symlink():
|
|
91
|
+
tree.add(f'{icon_map["符号链接"]} [yellow]{file.name}')
|
|
92
|
+
elif file.is_dir():
|
|
93
|
+
tree.add(f'{icon_map["目录"]} [cyan]{file.name}')
|
|
94
|
+
elif file.suffix in archive_files:
|
|
95
|
+
tree.add(f'{icon_map["归档文件(压缩包)"]} [red]{file.name}')
|
|
96
|
+
elif file.suffix == '.py':
|
|
97
|
+
tree.add(f'{icon_map["python脚本"]} [green]{file.name}')
|
|
98
|
+
elif is_executable(file):
|
|
99
|
+
tree.add(f'{icon_map["可执行文件"]} [green]{file.name}')
|
|
100
|
+
else:
|
|
101
|
+
tree.add(f'{icon_map["文件"]} [white]{file.name}')
|
|
102
|
+
console.print(Panel(tree, expand=False))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_executable(path: Path) -> bool:
|
|
106
|
+
path = Path(path)
|
|
107
|
+
if os.name == 'nt':
|
|
108
|
+
return path.suffix in executable_extensions
|
|
109
|
+
return path.is_file() and os.access(path, os.X_OK)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def long_list():
|
|
113
|
+
"""
|
|
114
|
+
列出目录下的所有文件及子目录信息(类似ll命令)
|
|
115
|
+
"""
|
|
116
|
+
args = parse_args()
|
|
117
|
+
target_dir = args.target_dir
|
|
118
|
+
|
|
119
|
+
target_dir = Path(target_dir)
|
|
120
|
+
|
|
121
|
+
table_title = f"目录 '{target_dir.resolve()}' 的内容"
|
|
122
|
+
table = Table(title=f"[bold cyan]{table_title}[/bold cyan]", show_lines=True)
|
|
123
|
+
table.add_column("名称", justify='center', vertical='middle', no_wrap=True)
|
|
124
|
+
table.add_column("类型", justify='center', vertical='middle')
|
|
125
|
+
table.add_column("权限", justify='center', vertical='middle')
|
|
126
|
+
table.add_column("大小", justify='center', vertical='middle')
|
|
127
|
+
table.add_column("最后修改时间", justify='center', vertical='middle')
|
|
128
|
+
for file in target_dir.iterdir():
|
|
129
|
+
item_stat = file.stat()
|
|
130
|
+
file_st_size = convert_bytes(item_stat.st_size)
|
|
131
|
+
file_name = file.name
|
|
132
|
+
if file.is_symlink():
|
|
133
|
+
file_st_size = '-'
|
|
134
|
+
txt_color = 'yellow'
|
|
135
|
+
item_type = "符号链接"
|
|
136
|
+
file_name = f'{file}(-> {file.readlink()})'
|
|
137
|
+
elif file.is_dir():
|
|
138
|
+
file_st_size = '-'
|
|
139
|
+
txt_color = 'cyan'
|
|
140
|
+
item_type = '目录'
|
|
141
|
+
elif file.is_file() and file.suffix in archive_files:
|
|
142
|
+
txt_color = 'red'
|
|
143
|
+
item_type = '归档文件(压缩包)'
|
|
144
|
+
elif is_executable(file):
|
|
145
|
+
txt_color = 'green'
|
|
146
|
+
item_type = '可执行文件'
|
|
147
|
+
else:
|
|
148
|
+
txt_color = 'white'
|
|
149
|
+
item_type = '文件'
|
|
150
|
+
|
|
151
|
+
mtime_timestamp = item_stat.st_mtime
|
|
152
|
+
mtime_str = datetime.fromtimestamp(mtime_timestamp).strftime('%Y-%m-%d %H:%M:%S')
|
|
153
|
+
item_permissions = get_permissions(file)
|
|
154
|
+
table.add_row(f"[bold {txt_color}]{file_name}", f'[{txt_color}]{item_type}', f'[{txt_color}]{item_permissions}',
|
|
155
|
+
f'[{txt_color}]{file_st_size}', f'[{txt_color}]{mtime_str}')
|
|
156
|
+
console.print(table)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def parse_args():
|
|
160
|
+
parser = argparse.ArgumentParser(description='列出目录下的所有文件及子目录信息')
|
|
161
|
+
parser.add_argument('target_dir', type=str, nargs='?', default='.', help='目标目录')
|
|
162
|
+
args = parser.parse_args()
|
|
163
|
+
return args
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == '__main__':
|
|
167
|
+
list_directory()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xstt"
|
|
7
|
+
version = "2026.04.23"
|
|
8
|
+
description = "一些终端实用命令行工具"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"python-pyper",
|
|
13
|
+
"rich",
|
|
14
|
+
"typer-slim[standard]",
|
|
15
|
+
'rapidfuzz',
|
|
16
|
+
'pypinyin'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
wcp = "fast_cp:main"
|
|
21
|
+
wdu = "disk_usage:main"
|
|
22
|
+
wll = "ls:long_list"
|
|
23
|
+
wls = "ls:list_directory"
|
|
24
|
+
wrm = "rm:main"
|
|
25
|
+
sq = "book_mark:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
py-modules = ["fast_cp", "disk_usage", 'ls', 'rm', 'book_mark']
|
|
29
|
+
|
|
30
|
+
[tool.pixi.workspace]
|
|
31
|
+
channels = ["conda-forge"]
|
|
32
|
+
platforms = ["win-64", 'linux-64']
|
|
33
|
+
|
|
34
|
+
[tool.pixi.pypi-dependencies]
|
|
35
|
+
ttxs = { path = ".", editable = true }
|
|
36
|
+
|
|
37
|
+
[tool.pixi.dependencies]
|
|
38
|
+
python = "3.10.*"
|
xstt-2026.4.23/rm.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Author: xiash$
|
|
3
|
+
Date: 2025/5/17$
|
|
4
|
+
Project: terminal_tools$
|
|
5
|
+
Description: $
|
|
6
|
+
***
|
|
7
|
+
* _ooOoo_
|
|
8
|
+
* o8888888o
|
|
9
|
+
* 88" . "88
|
|
10
|
+
* (| -_- |)
|
|
11
|
+
* O\ = /O
|
|
12
|
+
* ___/`---'\____
|
|
13
|
+
* . ' \\| |// `.
|
|
14
|
+
* / \\||| : |||// \
|
|
15
|
+
* / _||||| -:- |||||- \
|
|
16
|
+
* | | \\\ - /// | |
|
|
17
|
+
* | \_| ''\---/'' | |
|
|
18
|
+
* \ .-\__ `-` ___/-. /
|
|
19
|
+
* ___`. .' /--.--\ `. . __
|
|
20
|
+
* ."" '< `.___\_<|>_/___.' >'"".
|
|
21
|
+
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
|
|
22
|
+
* \ \ `-. \_ __\ /__ _/ .-` / /
|
|
23
|
+
* ======`-.____`-.___\_____/___.-`____.-'======
|
|
24
|
+
* `=---='
|
|
25
|
+
* .............................................
|
|
26
|
+
* 佛曰:bug泛滥,我已瘫痪!
|
|
27
|
+
"""
|
|
28
|
+
import argparse
|
|
29
|
+
import shutil
|
|
30
|
+
import os
|
|
31
|
+
import stat
|
|
32
|
+
|
|
33
|
+
from rich.progress import Progress, TimeElapsedColumn
|
|
34
|
+
from rich import print
|
|
35
|
+
from pyper import task
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
progress = Progress(
|
|
39
|
+
*Progress.get_default_columns(),
|
|
40
|
+
TimeElapsedColumn(),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def remove_force(path: Path | str):
|
|
45
|
+
"""
|
|
46
|
+
删除某个文件或整个目录
|
|
47
|
+
"""
|
|
48
|
+
path = Path(path)
|
|
49
|
+
if path.is_file() or path.is_symlink():
|
|
50
|
+
try:
|
|
51
|
+
path.unlink()
|
|
52
|
+
except PermissionError:
|
|
53
|
+
os.chmod(path, stat.S_IWRITE)
|
|
54
|
+
try:
|
|
55
|
+
path.unlink()
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f'注意:[yellow]{e}')
|
|
58
|
+
else:
|
|
59
|
+
try:
|
|
60
|
+
shutil.rmtree(path, onerror=remove_readonly)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f'注意:[yellow]{e}')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def fast_rm(path: Path | str, workers: int = 4):
|
|
66
|
+
"""
|
|
67
|
+
快速删除文件或整个目录
|
|
68
|
+
"""
|
|
69
|
+
path = Path(path)
|
|
70
|
+
progress_bar = progress.add_task('[green]删除中...', total=None)
|
|
71
|
+
progress.start()
|
|
72
|
+
final_desc = f'[purple]{path} 删除完成!'
|
|
73
|
+
if path.is_file() or path.is_symlink():
|
|
74
|
+
remove_force(path)
|
|
75
|
+
progress.update(progress_bar, total=1, advance=1, description=final_desc)
|
|
76
|
+
else:
|
|
77
|
+
total = len(os.listdir(path))
|
|
78
|
+
progress.update(progress_bar, total=total + 1)
|
|
79
|
+
if total > 0:
|
|
80
|
+
workers = min(total, workers)
|
|
81
|
+
pipeline = (
|
|
82
|
+
task(path.iterdir, branch=True) |
|
|
83
|
+
task(remove_force, workers=workers)
|
|
84
|
+
)
|
|
85
|
+
for _ in pipeline():
|
|
86
|
+
progress.advance(progress_bar)
|
|
87
|
+
shutil.rmtree(path, onerror=remove_readonly)
|
|
88
|
+
progress.update(progress_bar, advance=1, description=final_desc)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def remove_readonly(func, path, exc_info):
|
|
92
|
+
"""
|
|
93
|
+
用于处理权限问题
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
os.chmod(path, stat.S_IWRITE)
|
|
97
|
+
func(path)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f'注意:[yellow]{e}')
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def gather_paths(paths: list):
|
|
103
|
+
"""
|
|
104
|
+
将路径列表中的路径进行展开,并返回一个列表
|
|
105
|
+
"""
|
|
106
|
+
for path in paths:
|
|
107
|
+
path = Path(path)
|
|
108
|
+
yield path
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def main():
|
|
112
|
+
parser = argparse.ArgumentParser()
|
|
113
|
+
parser.add_argument('path', nargs='+', type=str, help='要删除的文件或目录路径')
|
|
114
|
+
parser.add_argument('-t', '--threads', type=int, default=4, help='删除文件时并发线程数')
|
|
115
|
+
args = parser.parse_args()
|
|
116
|
+
num_paths = len(args.path)
|
|
117
|
+
if num_paths == 0:
|
|
118
|
+
print('[red]没有要删除的文件或目录!')
|
|
119
|
+
else:
|
|
120
|
+
workers = min(num_paths, os.cpu_count() * 2)
|
|
121
|
+
pipeline = (
|
|
122
|
+
task(gather_paths, branch=True) |
|
|
123
|
+
task(fast_rm, workers=workers)
|
|
124
|
+
)
|
|
125
|
+
for _ in pipeline(args.path):
|
|
126
|
+
pass
|
|
127
|
+
progress.stop()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == '__main__':
|
|
131
|
+
main()
|
xstt-2026.4.23/setup.cfg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xstt
|
|
3
|
+
Version: 2026.4.23
|
|
4
|
+
Summary: 一些终端实用命令行工具
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: python-pyper
|
|
8
|
+
Requires-Dist: rich
|
|
9
|
+
Requires-Dist: typer-slim[standard]
|
|
10
|
+
Requires-Dist: rapidfuzz
|
|
11
|
+
Requires-Dist: pypinyin
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
book_mark.py
|
|
3
|
+
disk_usage.py
|
|
4
|
+
fast_cp.py
|
|
5
|
+
ls.py
|
|
6
|
+
pyproject.toml
|
|
7
|
+
rm.py
|
|
8
|
+
xstt.egg-info/PKG-INFO
|
|
9
|
+
xstt.egg-info/SOURCES.txt
|
|
10
|
+
xstt.egg-info/dependency_links.txt
|
|
11
|
+
xstt.egg-info/entry_points.txt
|
|
12
|
+
xstt.egg-info/requires.txt
|
|
13
|
+
xstt.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|