3 # This is a wrapper around "ls" that results in tabbed output that's suitable
6 import subprocess, sys, os, re, shutil
8 def display_length(string):
9 num_displayed_chars = 0
10 in_escape_sequence = False
12 if in_escape_sequence:
13 if ord(c) >= 0x40 and ord(c) <= 0x7E and c != '[':
14 in_escape_sequence = False
16 in_escape_sequence = True
18 num_displayed_chars += 1
19 return num_displayed_chars
21 def decode(raw_bytes):
23 return str(raw_bytes, encoding = "utf-8")
25 return str(raw_bytes, encoding = "latin1")
28 # Columnation, following how "ls" does it.
31 column_separator_width_estimate = 5
34 def __init__(self, index):
35 self.valid_length = True
36 self.line_length = (index + 1) * min_column_width
37 self.col_arr = [ min_column_width ] * (index + 1)
39 def columnate(filenames):
40 # Set up for columnation.
41 line_length = shutil.get_terminal_size().columns
42 max_possible_columns = int(line_length / min_column_width)
43 if (line_length % min_column_width) != 0:
44 max_possible_columns += 1
45 max_cols = min(max_possible_columns, len(filenames))
47 for index in range(max_cols):
48 column_info.append(ColumnInfo(index))
50 # Compute the maximum number of possible columns.
51 # "Compute" here means "try all the possible numbers of columns".
52 num_files = len(filenames)
55 for which_file in range(num_files):
56 filename = filenames[which_file]
57 name_length = display_length(filename)
58 for cur_num_columns in range(1, max_cols + 1):
59 cur_column_info = column_info[cur_num_columns - 1]
60 if cur_column_info.valid_length:
61 which_column = int(which_file / ((num_files + cur_num_columns -1) / cur_num_columns))
62 real_length = name_length
63 if which_column != cur_num_columns - 1:
64 real_length += column_separator_width_estimate
65 if cur_column_info.col_arr[which_column] < real_length:
66 cur_column_info.line_length += real_length - cur_column_info.col_arr[which_column]
67 cur_column_info.col_arr[which_column] = real_length
68 cur_column_info.valid_length = cur_column_info.line_length < line_length
69 # Find the largest number of columns that fit.
70 num_columns = max_cols
71 while num_columns > 1:
72 if column_info[num_columns - 1].valid_length:
76 # Now output the filenames.
77 num_rows = int(num_files / num_columns)
78 if num_files % num_columns != 0:
80 print("\x1B[?5001h", end = '')
81 for which_row in range(num_rows):
83 which_file = which_row
85 print(filenames[which_file], end = '')
86 which_file += num_rows
87 if which_file >= num_files:
91 print("\x1B[?5001l", end = '')
98 first_line_summary = False
99 for arg in sys.argv[1:]:
100 if arg[0:2] != "--" and arg[0] == "-":
103 first_line_summary = True
105 first_line_summary = True
108 # Find "ls" in the path.
110 for path in os.environ["PATH"].split(":"):
111 file_path = path + "/ls"
112 if os.path.isfile(file_path) and os.access(file_path, os.X_OK):
113 ls_binary = file_path
116 printf(f"Couldn't find \"ls\"!", file = sys.stderr)
119 process = subprocess.Popen([ls_binary] + ls_args + ["-1"], stdout = subprocess.PIPE)
120 result = process.communicate()
123 long_re = re.compile('([\w-]+)\s+(\d+)\s+(\w+)\s+(\w+)\s+(\S+)\s+(\w+\s+\d+\s+[\d:]+)\s+([^\r\n]+)')
125 filenames = result[0].splitlines()
126 if first_line_summary and len(filenames) > 0:
127 print(decode(filenames[0]))
128 filenames = filenames[1:]
129 print("\x1B[?5001h", end = '')
130 for line in filenames:
132 match = long_re.match(line)
134 print('\t'.join(match.groups()))
137 print("\x1B[?5001l", end = '')
138 except BrokenPipeError:
139 # We can't do anything about BrokenPipeError; just be quiet about it.
142 filenames = result[0].splitlines()
143 if first_line_summary and len(filenames) > 0:
144 print(decode(filenames[0]))
145 filenames = filenames[1:]
147 for line in filenames:
149 if len(line) == 0 or line[-1] == ":":
150 columnate(cur_filenames)
154 cur_filenames.append(line)
156 columnate(cur_filenames)
157 except BrokenPipeError:
158 # We can't do anything about BrokenPipeError; just be quiet about it.
161 sys.exit(process.returncode)