Introduction
Hey all. I was recently asked this question:
describe what happens when
ls -l *
is run on the Linux command line
My description was very wrong. But, now that I've got some time and room to learn a bit more, I've taken a few minutes to write up the actual answer.
Listing in Bash on Linux
The Actual Output
Here's an example of the output:
➜ pwd $ ls -l *
-rw-r--r--@ 1 user group 0 Month 01 14:05 LICENSE
-rw-r--r--@ 1 user group 202 Month 01 14:05 go.mod
-rw-r--r--@ 1 user group 896 Month 01 14:05 go.sum
-rw-r--r--@ 1 user group 138 Month 01 14:05 main.go
cmd:
total 8
-rw-r--r--@ 1 user group 1444 Month 01 14:05 root.go
internal:
total 16
-rw-r--r--@ 1 user group 37 Month 01 16:05 main.go
-rw-r--r--@ 1 user group 225 Month 01 14:10 main_test.go
Here are some things we see so far:
- It shows the current working directory and all its non-hidden contents.
- It shows permissions of the files in symbolic notation (as opposed to file mode notation like 0755). These permissions include owner, group, and other permissions (in that order) along with
@
indicating extended attributes like for SELinux. - It shows the total number of child elements below each directory child below the current working directory (pwd).
- This listing shows the user and group indicated on the child (file or dir)
- The last mod date/time for the file.
How does ls
get this information?
-
stat
- for getting file and directory info for each item listed, even with symbolic links and not using the-l
(long-listing) option. -
lstat
- for getting metadata information for when using-l
(long-listing) on symbolic link file types.
Code Sample
In an attempt to implement ls
with -l
support in go, here is a code sample.
Here's the stat_t
parsing section:
func New(n string, stat *syscall.Stat_t) Stat {
var m Stat
m.BaseName = path.Base(n)
m.AbsolutePath, _ = filepath.Abs(n)
if stat.Mode&syscall.S_IFDIR == syscall.S_IFDIR {
m.Type = "directory"
} else if stat.Mode&syscall.S_IFREG == syscall.S_IFREG {
m.Type = "file"
} else if stat.Mode&syscall.S_IFLNK == syscall.S_IFLNK {
m.Type = "symlink"
} else if stat.Mode&syscall.S_IFIFO == syscall.S_IFIFO {
m.Type = "fifo"
} else if stat.Mode&syscall.S_IFSOCK == syscall.S_IFSOCK {
m.Type = "socket"
} else if stat.Mode&syscall.S_IFCHR == syscall.S_IFCHR {
m.Type = "character_device"
} else if stat.Mode&syscall.S_IFBLK == syscall.S_IFBLK {
m.Type = "block_device"
} else {
m.Type = "unknown"
}
m.SizeBytes = stat.Size
m.Mode = stat.Mode
m.UserID = stat.Uid
m.GroupID = stat.Gid
m.LastAccessedTime = time.Unix(stat.Atimespec.Sec, stat.Atimespec.Nsec)
m.LastModifiedTime = time.Unix(stat.Mtimespec.Sec, stat.Mtimespec.Nsec)
m.CreateTime = time.Unix(stat.Ctimespec.Sec, stat.Ctimespec.Nsec)
m.BirthTime = time.Unix(stat.Birthtimespec.Sec, stat.Birthtimespec.Nsec)
m.BlockSize = uint32(stat.Blksize)
m.NumBlocks = uint64(stat.Blocks)
var err error
u, err := user.LookupId(fmt.Sprintf("%d", stat.Uid))
if err == nil {
m.Owner = u.Username
m.UserName = u.Username
} else {
m.Owner = "unknown"
m.UserName = "unknown"
}
u, err = user.LookupId(fmt.Sprintf("%d", stat.Gid))
if err == nil {
m.GroupName = u.Username
} else {
m.GroupName = "unknown"
}
m.HardLinkReferenceCount = uint16(stat.Nlink)
octalPerm := os.FileMode(stat.Mode) & os.ModePerm
m.Permissions.Octal = fmt.Sprintf("%o", octalPerm)
const (
ownerStatTOffset = 6
groupStatTOffset = 3
otherStatTOffset = 1
)
// https://man7.org/linux/man-pages/man7/inode.7.html
// who-has-what-perms section starts at offset 6 and ends at offset 10.
// The >> operator in Go shifts the bits of a number to the right.
// Think of the number as a row of lights (1 = on, 0 = off).
// - >> 1 moves all the lights 1 step to the right, filling empty spaces on the left:
// - Positive numbers fill with 0 (e.g., 8 >> 1: 00001000 -> 00000100 = 4).
// - Negative numbers fill with 1 to keep the number negative.
// - Bits that fall off the right edge disappear.
var (
ownerPerms = uint8(octalPerm >> ownerStatTOffset)
groupPerms = uint8(octalPerm >> groupStatTOffset)
otherPerms = uint8(octalPerm >> otherStatTOffset)
)
m.Permissions.Symbolic.Owner = perm.New(ownerPerms)
m.Permissions.Symbolic.Group = perm.New(groupPerms)
m.Permissions.Symbolic.Other = perm.New(otherPerms)
return m
}
And here is the permissions parsing code:
func New(mode uint8) SymbolicPermission {
var (
read bool
write bool
execute bool
)
const (
readOffset = 4
writeOffset = 2
executeOffset = 1
)
// bitIsSet determines whether a specific bit is set in an unsigned int.
// Example:
// mode := uint8(5) // Binary: 101
// bitIsSet(mode, 4) // Returns true (read bit is set)
// 0b101 & 0b100 = 0b100 (comparison: 0b100 == 0b100 → true)
// bitIsSet(mode, 2) // Returns false (write bit is not set)
// 0b101 & 0b010 = 0b000 (comparison: 0b000 == 0b010 → false)
// bitIsSet(mode, 1) // Returns true (execute bit is set)
// 0b101 & 0b001 = 0b001 (comparison: 0b001 == 0b001 → true)
bitIsSet := func(mode, offset uint8) bool {
return mode&offset == offset
}
read = bitIsSet(mode, readOffset)
write = bitIsSet(mode, writeOffset)
execute = bitIsSet(mode, executeOffset)
return SymbolicPermission{
Read: read,
Write: write,
Execute: execute,
}
}
Top comments (0)