#!/usr/bin/env ruby # fls - a mix of find(1) and ls(1) # TODO: xdev, stat caching require 'find' require 'etc' require 'optparse' class Reversed < Struct.new(:object) def <=>(other) -(self.object <=> other.object) end end sort = "N" fmt = "%p%_%l%_%u%_%g%_%h%_%m%_%n" # roghly, ls -lha maxdepth = nil exclude = [] opt_parser = OptionParser.new("", 20, ' ') do |opts| opts.banner = "Usage: fls [-x GLOB] [-f FMT] DIR..." opts.on("-f FMT", "output format", " %n filename %N raw filename", " %b basename %B raw basename", " %u user %U uid %g group %G gid", " %s size %h human size %F indicator (*/=|)", " %p modestring %P octal mode", " %i inode number %l number of hardlinks", " %e extension %E name without extension", " %a iso atime %A epoch atime", " %m iso mtime %M epoch mtime", " %c iso ctime %C epoch ctime", " %_ column alignment" ) { |f| fmt = f } opts.on("-s SORTKEY...", "sort order (prepend - to reverse order)", " n filename b basename s size", " u user U uid g group G gid", " i inode number l number of hardlinks", " e extension E name without extension", " a atime m mtime c ctime" ) { |s| sort = s } opts.on("-m DEPTH", "max depth (number of / in pathname allowed)") { |m| maxdepth = Integer(m) } opts.on("-x EXCLUDE", "exclude GLOB (** for recursive *)") { |x| exclude << x } opts.on_tail("-h","--help", "Show this message") do puts opts exit end end opt_parser.parse! ARGV files = [] ARGV.replace ["."] if ARGV.empty? ARGV.each { |arg| depthoffset = arg.count('/') Find.find(arg) { |path| next if path == "." || path == ".." path.sub!(/\A.\//, '') if maxdepth && path.count('/') > maxdepth+depthoffset Find.prune end unless exclude.any? { |glob| File.fnmatch(glob, path, File::FNM_PATHNAME) } files << path end } } def sortkey(fmt, file) stat = File.lstat(file) fmt.scan(/(-?)(.)/).map { |r, f| key = case f when "N", "n"; file when "u"; Etc.getpwuid(stat.uid).name when "U"; "%d" % stat.uid when "g"; Etc.getgrgid(stat.gid).name when "G"; "%d" % stat.gid when "s", "h"; stat.size when "i"; stat.ino when "p", "P"; stat.mode when "l"; stat.nlink when "B", "b"; esc(File.basename(file)) when "a", "A"; stat.atime when "c", "C"; stat.ctime when "m", "M"; stat.mtime when "e"; file.index(".") ? file[/[^.]*\Z/] : "" when "E"; file.index(".") ? file[/(.*)\./, 1] : file else abort "Unknown sort key '#{r}#{f}'" end key = Reversed.new(key) if r == "-" key } end def esc(str) str.dump[1...-1] end def human(size) units = %w{B K M G T P E Z Y} until size < 1024 units.shift size /= 1024.0 end "%d%s" % [size, units.first] end # Inspired by busybox. def modestring(mode) buf = "0pcCd?bB-?l?s???"[(mode >> 12) & 0x0f, 1] buf << (mode & 00400 == 0 ? "-" : "r") buf << (mode & 00200 == 0 ? "-" : "w") buf << (mode & 04000 == 0 ? (mode & 00100 == 0 ? "-" : "x") : (mode & 00100 == 0 ? "S" : "s")) buf << (mode & 00040 == 0 ? "-" : "r") buf << (mode & 00020 == 0 ? "-" : "w") buf << (mode & 02000 == 0 ? (mode & 00010 == 0 ? "-" : "x") : (mode & 00010 == 0 ? "S" : "s")) buf << (mode & 00004 == 0 ? "-" : "r") buf << (mode & 00002 == 0 ? "-" : "w") buf << (mode & 01000 == 0 ? (mode & 00001 == 0 ? "-" : "x") : (mode & 00001 == 0 ? "T" : "t")) end def iso(time) time.strftime("%Y-%m-%d %H:%M:%S") end MAGIC = "\000\007\000" def format(fmt, file) stat = File.lstat(file) fmt.gsub(/%(-?)(.)/){ r, f = $1, $2 case f when "u"; Etc.getpwuid(stat.uid).name when "U"; "%d" % stat.uid when "g"; Etc.getgrgid(stat.gid).name when "G"; "%d" % stat.gid when "s"; "%d" % stat.size when "h"; human(stat.size) when "i"; stat.ino when "p"; modestring(stat.mode) when "P"; "%06o" % stat.mode when "n"; esc(file) when "N"; file when "l"; stat.nlink when "F"; if stat.symlink?; "" elsif stat.directory?; "/" elsif stat.executable?; "*" elsif stat.socket?; "=" elsif stat.pipe?; "|" else "" end when "b"; esc(File.basename(file)) when "B"; File.basename(file) when "a"; iso(stat.atime) when "A"; stat.atime.to_i.to_s when "c"; iso(stat.ctime) when "C"; stat.ctime.to_i.to_s when "m"; iso(stat.mtime) when "M"; stat.mtime.to_i.to_s when "e"; file.index(".") ? file[/[^.]*\Z/] : "" when "E"; file.index(".") ? file[/(.*)\./, 1] : file when "_"; MAGIC else abort "Unknown format key %#{r}#{f}" end } end def columnate(strs) return "" if strs.empty? fields = strs.map { |s| s.split(MAGIC) } widths = fields.transpose.map { |cols| cols.map { |f| f.size }.max } widths[widths.size-1] = 0 # no trailing spaces fields.map { |line| line.zip(widths).map { |f, w| if f =~ /\A[0-9]+[BKMGTPEZY]?\Z/ f.rjust(w) else f.ljust(w) end }.join(" ") + "\n" }.join end print columnate(files.sort_by { |file| sortkey(sort, file) }.map { |file| format(fmt, file) })