ls.ha (9111B)
1 use fmt; 2 use fs; 3 use getopt; 4 use main; 5 use os; 6 use sort; 7 use strings; 8 use time; 9 use time::chrono; 10 use time::date; 11 use unix::passwd; 12 13 type tstamp = (atime | ctime | mtime); 14 type atime = void; // access time 15 type ctime = void; // creation time 16 type mtime = void; // modification time 17 18 type sorttype = (name | time | filesize); 19 type name = void; 20 type time = void; 21 type filesize = void; 22 23 type entry = struct { 24 name: str, 25 mask: fs::stat_mask, 26 mode: fs::mode, 27 uid: uint, 28 gid: uint, 29 sz: size, 30 inode: u64, 31 time: time::instant, 32 target_mode: fs::mode, 33 target_inode: u64, 34 }; 35 36 type flags = struct { 37 detailed: bool, 38 detailedid: bool, 39 fileindicator: bool, 40 listdir: bool, 41 printinode: bool, 42 recurse: bool, 43 reverseorder: bool, 44 showhidden: bool, 45 showhiddenexcept: bool, 46 sortby: sorttype, 47 timestamp: tstamp, 48 unsorted: bool, 49 usetargetinfo: bool, 50 }; 51 52 type dirstate = struct { 53 listdir: bool, 54 firstdir: bool, 55 showdir: bool, 56 }; 57 58 export fn utilmain() (void | main::error) = { 59 const help: []getopt::help = [ 60 "list directory contents", 61 ('A', "list hidden files except '.' and '..'"), 62 ('a', "list hidden files"), 63 ('c', "use ctime"), 64 ('d', "list directories themselves"), 65 ('F', "append a type indicator to filename"), 66 ('L', "show information for the target of the link"), 67 ('i', "print the index number for every file"), 68 ('l', "list detailed information"), 69 ('n', "like '-l' but with numeric IDs"), 70 ('R', "list recursively"), 71 ('r', "reverse order"), 72 ('S', "sort by size in decreasing order"), 73 ('t', "sort by timestamp"), 74 ('U', "unsorted"), 75 ('u', "use atime"), 76 "[dir...]", 77 ]; 78 const cmd = getopt::parse(os::args, help...); 79 defer getopt::finish(&cmd); 80 81 let flg = flags{ sortby = name, timestamp = mtime, ... }; 82 for (let i = 0z; i < len(cmd.opts); i += 1) { 83 const opt = cmd.opts[i]; 84 switch (opt.0) { 85 case 'A' => 86 flg.showhiddenexcept = true; 87 case 'a' => 88 flg.showhidden = true; 89 case 'c' => 90 flg.sortby = time; 91 flg.timestamp = ctime; 92 case 'd' => 93 flg.listdir = true; 94 case 'F' => 95 flg.fileindicator = true; 96 case 'i' => 97 flg.printinode = true; 98 case 'L' => 99 flg.usetargetinfo = true; 100 case 'l' => 101 flg.detailed = true; 102 case 'n' => 103 flg.detailed = true; 104 flg.detailedid = true; 105 case 'R' => 106 flg.recurse = true; 107 case 'r' => 108 flg.reverseorder = true; 109 case 't' => 110 flg.sortby = time; 111 flg.timestamp = mtime; 112 case 'S' => 113 flg.sortby = filesize; 114 case 'U' => 115 flg.unsorted = true; 116 case 'u' => 117 flg.sortby = time; 118 flg.timestamp = atime; 119 case => 120 main::usage(help); 121 }; 122 }; 123 124 let state = dirstate{...}; 125 state.firstdir = true; 126 switch (len(cmd.args)) { 127 case 0 => 128 const ent: entry = mkent(".", flg)?; 129 state.listdir = shouldlistdir(ent, flg); 130 ls("", ent, &state, flg)?; 131 case 1 => 132 const path = cmd.args[0]; 133 const ent: entry = mkent(path, flg)?; 134 state.listdir = shouldlistdir(ent, flg); 135 ls("", ent, &state, flg)?; 136 case => 137 let fileents: []entry = []; 138 let dirents: []entry = []; 139 let fs = 0z, ds = 0z; 140 defer free(fileents); 141 defer free(dirents); 142 143 for (let i = 0z; i < len(cmd.args); i += 1z) { 144 const ent = mkent(cmd.args[i], flg)?; 145 if (!flg.listdir && fs::isdir(ent.mode)) { 146 append(dirents, ent); 147 ds += 1; 148 } else { 149 append(fileents, ent); 150 fs += 1; 151 }; 152 }; 153 154 state.showdir = ds > 1 || (ds > 0 && fs > 0); 155 if (!flg.unsorted) { 156 ls_sortents(fileents, flg); 157 ls_sortents(dirents, flg); 158 }; 159 160 for (let i = 0z; i < len(fileents); i += 1z) { 161 state.listdir = false; 162 ls("", fileents[i], &state, flg)?; 163 }; 164 if (len(fileents) > 0 && len(dirents) > 0) fmt::printf("\n")?; 165 for (let i = 0z; i < len(dirents); i += 1z) { 166 state.listdir = true; 167 ls("", dirents[i], &state, flg)?; 168 }; 169 }; 170 }; 171 172 fn mkent(path: str, flg: flags) (entry | main::error) = { 173 const stat = if (flg.usetargetinfo) { 174 yield os::stat(os::realpath(path)?)?; 175 } else { 176 yield os::stat(path)?; 177 }; 178 179 let f = entry { 180 name = path, 181 mask = stat.mask, 182 mode = stat.mode, 183 uid = stat.uid, 184 gid = stat.gid, 185 sz = stat.sz, 186 inode = stat.inode, 187 time = match (flg.timestamp) { 188 case atime => 189 yield stat.atime; 190 case ctime => 191 yield stat.ctime; 192 case mtime => 193 yield stat.mtime; 194 }, 195 ... 196 }; 197 if (fs::islink(stat.mode)) { 198 let targetstat = os::stat(os::readlink(f.name)?)?; 199 f.target_mode = targetstat.mode; 200 f.target_inode = targetstat.inode; 201 }; 202 return f; 203 }; 204 205 fn ls(path: str, ent: entry, st: *dirstate, flg: flags) (void | main::error) = { 206 if (!st.listdir) { 207 ls_printent(ent, flg)?; 208 } else if (fs::isdir(ent.mode) || (fs::islink(ent.mode) 209 && fs::isdir(ent.target_mode))) { 210 if (st.firstdir) { 211 st.firstdir = false; 212 } else { 213 fmt::printf("\n")?; 214 }; 215 lsdir(path, ent, st, flg)?; 216 }; 217 }; 218 219 fn lsdir( 220 path: str, 221 dirent: entry, 222 st: *dirstate, 223 flg: flags, 224 ) (void | main::error) = { 225 const rootdir = os::getcwd(); 226 const dir = os::readdir(dirent.name)?; 227 let ents: []entry = []; 228 defer fs::dirents_free(dir); 229 defer free(ents); 230 231 os::chdir(dirent.name)?; 232 for (let i = 0z; i < len(dir); i += 1) { 233 const path = dir[i].name; 234 if (strings::hasprefix(path, ".") && !flg.showhidden 235 && !flg.showhiddenexcept) { 236 continue; 237 } else if (flg.showhiddenexcept) { 238 if (strings::compare(path, "..") == 0 || 239 strings::compare(path, ".") == 0) { 240 continue; 241 }; 242 }; 243 append(ents, mkent(dir[i].name, flg)?); 244 }; 245 246 if (!flg.unsorted) ls_sortents(ents, flg); 247 248 if (path != "" || st.showdir) fmt::printfln("{}{}:", path, dirent.name)?; 249 for (let i = 0z; i < len(ents); i += 1) { 250 ls_printent(ents[i], flg)?; 251 }; 252 253 if (flg.recurse) { 254 for (let i = 0z; i < len(ents); i += 1z) { 255 const ent = ents[i]; 256 if (strings::compare(ent.name, "..") == 0 || 257 strings::compare(ent.name, ".") == 0) { 258 continue; 259 }; 260 if (fs::islink(ent.mode) && fs::isdir(ent.target_mode) && 261 !flg.usetargetinfo) { 262 continue; 263 }; 264 265 const prefix = strings::concat(path, dirent.name, "/"); 266 st.listdir = true; 267 ls(prefix, ents[i], st, flg)?; 268 free(prefix); 269 }; 270 }; 271 272 os::chdir(rootdir)?; 273 }; 274 275 fn ls_sortents(ents: []entry, flg: flags) void = { 276 match (flg.sortby) { 277 case name => 278 sort::sort(ents, size(entry), &namecmp); 279 case time => 280 sort::sort(ents, size(entry), &timecmp); 281 case filesize => 282 sort::sort(ents, size(entry), &sizecmp); 283 }; 284 if (flg.reverseorder) { 285 reverse(ents, size(entry)); 286 }; 287 }; 288 289 fn ls_printent(ent: entry, flg: flags) (void | main::error) = { 290 if (flg.printinode) fmt::printf("{} ", ent.inode)?; 291 if (flg.detailed) { 292 fmt::printf("{} ", fs::mode_str(ent.mode))?; 293 294 if (flg.detailedid) { 295 fmt::printf("{:-8} {:-8} ", ent.uid, ent.gid)?; 296 } else { 297 match (passwd::getuid(ent.uid)) { 298 case let pw: passwd::pwent => 299 fmt::printf("{:-8} ", pw.username)?; 300 passwd::pwent_finish(&pw); 301 case void => 302 fmt::printf("{:-8} ", ent.uid)?; 303 }; 304 305 match (passwd::getgid(ent.gid)) { 306 case let gr: passwd::grent => 307 fmt::printf("{:-8} ", gr.name)?; 308 passwd::grent_finish(&gr); 309 case void => 310 fmt::printf("{:-8} ", ent.gid)?; 311 }; 312 }; 313 314 fmt::printf("{:10} ", ent.sz)?; 315 316 const date = date::from_instant(chrono::LOCAL, ent.time); 317 const now = time::now(time::clock::REALTIME); 318 if (now.sec > ent.time.sec + (180 * 24 * 60 * 60)) { 319 date::format(os::stdout, "%b %d %Y", &date)!; 320 } else { 321 date::format(os::stdout, "%b %d %H:%M", &date)!; 322 }; 323 fmt::printf(" ")?; 324 }; 325 326 fmt::printf(ent.name)?; 327 if (flg.fileindicator) fmt::printf(ls_indicator(ent.mode))?; 328 if (fs::islink(ent.mode)) { 329 const target = os::readlink(ent.name)?; 330 fmt::printf(" -> {}", target)?; 331 if (flg.fileindicator) fmt::printf(ls_indicator(ent.target_mode))?; 332 }; 333 fmt::print("\n")?; 334 }; 335 336 fn ls_indicator(m: fs::mode) str = { 337 if (fs::isdir(m)) { 338 return "/"; 339 } else if (fs::isfifo(m)) { 340 return "@"; 341 } else if (fs::issocket(m)) { 342 return "="; 343 } else if (fs::isfile(m)) { 344 const perm = fs::mode_perm(m); 345 if (perm & fs::mode::USER_X == fs::mode::USER_X 346 || perm & fs::mode::GROUP_X == fs::mode::GROUP_X 347 || perm & fs::mode::OTHER_X == fs::mode::OTHER_X) { 348 return "*"; 349 }; 350 }; 351 return ""; 352 }; 353 354 fn shouldlistdir(e: entry, flg: flags) bool = { 355 return (!flg.listdir && fs::isdir(e.mode)) || 356 (fs::islink(e.mode) && fs::isdir(e.target_mode) && 357 (!flg.listdir && !flg.fileindicator && !flg.detailed)); 358 }; 359 360 fn timecmp(a: const *opaque, b: const *opaque) int = { 361 const a = a: *entry, b = b: *entry; 362 const dist = b.time.sec - a.time.sec; 363 if (dist > 0) { 364 return 1; 365 } else if (dist < 0) { 366 return -1; 367 } else { 368 return 0; 369 }; 370 }; 371 372 fn sizecmp(a: const *opaque, b: const *opaque) int = { 373 const a = a: *entry, b = b: *entry; 374 const asize = a.sz: int, bsize = b.sz: int; 375 const dist = bsize - asize; 376 if (dist > 0) { 377 return 1; 378 } else if (dist < 0) { 379 return -1; 380 } else { 381 return 0; 382 }; 383 }; 384 385 fn namecmp(a: const *opaque, b: const *opaque) int = { 386 const a = a: *entry, b = b: *entry; 387 return strings::compare(a.name, b.name); 388 }; 389 390 fn reverse(ents: []entry, itemsz: size) void = { 391 for (let i = 0z, r = len(ents) - 1; i < r) { 392 let l = ents[i]; 393 ents[i] = ents[r]; 394 ents[r] = l; 395 i += 1; 396 r -= 1; 397 }; 398 }; 399