-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathels.py
More file actions
executable file
·176 lines (156 loc) · 6.42 KB
/
els.py
File metadata and controls
executable file
·176 lines (156 loc) · 6.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/usr/bin/env python
########################################################################
# els.py: Extended File List with Detailed Timestamps
#
# Description:
# This script provides an extended file listing similar to `ls -l`, but
# includes additional timestamp information such as access time (atime),
# modification time (mtime), change time (ctime), and creation time (birth).
# It displays detailed file metadata, including permissions, size, owner,
# group, and timestamps, formatted for easy reading.
#
# Author: id774 (More info: http://id774.net)
# Source Code: https://github.com/id774/scripts
# License: The GPL version 3, or LGPL version 3 (Dual License).
# Contact: idnanashi@gmail.com
#
# Usage:
# Run the script without arguments to list the current directory:
# els.py
#
# Specify a directory path to list its contents:
# els.py /path/to/directory
#
# Requirements:
# - Python Version: 3.3 or later
#
# Notes:
# - This script retrieves timestamps using `os.stat()`. The `birth` time is
# available only on macOS (APFS, HFS+). On Linux (ext4), it will display "N/A".
# - File ownership is resolved using `pwd` and `grp` modules. If a user or group
# is not found, the UID/GID is displayed instead.
# - The file mode (permissions) is formatted using `stat.filemode()`, matching
# the style of `ls -l`.
# - The output is sorted by filename in ascending order.
# - Symbolic links are listed as regular files without following the link.
# - Large directories may take longer to process due to multiple system calls.
# - If a directory is not readable due to permissions, the script will display an error.
#
# Version History:
# v1.3 2025-07-01
# Standardized termination behavior for consistent script execution.
# v1.2 2025-06-23
# Unified usage output to display full script header and support common help/version options.
# v1.1 2025-04-13
# Unify log level formatting using [INFO], [WARN], and [ERROR] tags.
# v1.0 2025-01-31
# Initial release.
#
########################################################################
import grp
import os
import pwd
import stat
import sys
import time
# Exported functions for testing and external usage
__all__ = ["format_time", "get_owner", "get_group", "format_file_entry", "get_file_info"]
def usage():
""" Display the script header as usage information and exit. """
script_path = os.path.abspath(__file__)
in_header = False
try:
with open(script_path, 'r', encoding='utf-8') as f:
for line in f:
if line.strip().startswith('#' * 10):
if not in_header:
in_header = True
continue
else:
break
if in_header and line.startswith('#'):
if line.startswith('# '):
print(line[2:], end='')
else:
print(line[1:], end='')
except Exception as e:
print("Error reading usage information: %s" % str(e), file=sys.stderr)
sys.exit(1)
sys.exit(0)
def format_time(timestamp):
""" Convert timestamp to 'YYYY-MM-DD HH:MM:SS' format in local time """
if timestamp is None:
raise ValueError("Timestamp cannot be None")
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))
def get_owner(uid):
""" Convert UID to username """
try:
return pwd.getpwuid(uid).pw_name
except KeyError:
return str(uid)
def get_group(gid):
""" Convert GID to group name """
try:
return grp.getgrgid(gid).gr_name
except KeyError:
return str(gid)
def format_file_entry(path):
""" Retrieve and format file metadata for a single file """
st = os.stat(path)
return {
"mode": stat.filemode(st.st_mode),
"size": st.st_size,
"owner": get_owner(st.st_uid),
"group": get_group(st.st_gid),
"atime": format_time(st.st_atime), # Last access time
"mtime": format_time(st.st_mtime), # Last modification time
"ctime": format_time(st.st_ctime), # Last metadata change time
"birth": format_time(st.st_birthtime) if hasattr(st, "st_birthtime") else "N/A",
"name": os.path.basename(path)
}
def get_file_info(path="."):
""" Get file metadata, whether it's a single file or a directory listing """
if not os.path.exists(path):
return "[ERROR] '{}' does not exist.".format(path)
try:
if os.path.isfile(path):
return [format_file_entry(path)] # Return as a list for consistency
elif os.path.isdir(path):
# Check if os.scandir() exists before trying to use it
if hasattr(os, "scandir"):
entry = os.scandir(path) # Call to test existence
if hasattr(entry, "__exit__"): # Check if context manager is supported
with os.scandir(path) as entries:
return [format_file_entry(entry.path) for entry in sorted(entries, key=lambda e: e.name)]
# If os.scandir() is unavailable or lacks __exit__, fallback to os.listdir()
return [format_file_entry(os.path.join(path, entry)) for entry in sorted(os.listdir(path))]
else:
return "[ERROR] '{}' is neither a file nor a directory.".format(path)
except PermissionError:
return "[ERROR] Permission denied for '{}'.".format(path)
def main():
""" Main function to handle CLI execution """
target_path = sys.argv[1] if len(sys.argv) > 1 else "."
result = get_file_info(target_path)
if isinstance(result, str):
print(result)
sys.exit(1)
# Print header
print("{:<10} {:>10} {:<10} {:<10} {:<19} {:<19} {:<19} {:<19} {}".format(
"Mode", "Size", "Owner", "Group", "Atime", "Mtime", "Ctime", "Birth", "Name"
))
print("-" * 150)
# Print file entries
for entry in result:
print("{:<10} {:>10} {:<10} {:<10} {:<19} {:<19} {:<19} {:<19} {}".format(
entry["mode"], entry["size"], entry["owner"], entry["group"],
entry["atime"], entry["mtime"], entry["ctime"], entry["birth"], entry["name"]
))
return 0
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] in ('-h', '--help', '-v', '--version'):
usage()
if sys.version_info < (3, 3):
print("[ERROR] This script requires Python 3.3 or later.", file=sys.stderr)
sys.exit(9)
sys.exit(main())