DEV Community

sean2024
sean2024

Posted on

Day 10: ls -l *

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
Enter fullscreen mode Exit fullscreen mode

Here are some things we see so far:

  1. It shows the current working directory and all its non-hidden contents.
  2. 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.
  3. It shows the total number of child elements below each directory child below the current working directory (pwd).
  4. This listing shows the user and group indicated on the child (file or dir)
  5. 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
}
Enter fullscreen mode Exit fullscreen mode

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)