Skip to content

Instantly share code, notes, and snippets.

@grahameger
Last active May 12, 2025 08:47
Show Gist options
  • Save grahameger/2507019334f07036f84080a87684f4b3 to your computer and use it in GitHub Desktop.
Save grahameger/2507019334f07036f84080a87684f4b3 to your computer and use it in GitHub Desktop.
lsds.c – list block devices and selected metadata. C rewrite based on the Python utility by Tanel Poder <https://0x.tools>, 2025.
// SPDX-License-Identifier: GPL-2.0-or-later
// lsds.c – list block devices and selected metadata (C rewrite) with JSON output option
// Originally based on the Python utility by Tanel Poder <https://0x.tools>, 2025.
//
// Build: cc -Wall -Wextra -pedantic -std=c17 -D_POSIX_C_SOURCE=200809L -o lsds lsds.c
// Usage: ./lsds [-v] [-p] [-c COL1 COL2 …] [-a COL1,COL2,…] [-l]
// [-r] [-j]
// See --help for details.
//
// -------------------------------------------------------------
#define _GNU_SOURCE /* for getline */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdbool.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <limits.h>
#include <regex.h>
#include <getopt.h>
#include <ctype.h>
#define SYSFS_BASE "/sys/class/block"
#define MODULE_BASE "/sys/module"
#define SECTOR_SIZE 512ULL
#define VALUE_MISSING "-"
#define MAX_VAL_LEN 256 /* enough for sysfs scalar attrs */
#define MAX_COLUMNS 32
/* ---------------------------------------------------------- */
/* Column definition */
/* ---------------------------------------------------------- */
enum src_type {
SRC_DEVNAME,
SRC_FILE,
SRC_SIZE,
SRC_SCHED,
SRC_TYPE,
SRC_QDEPTH,
};
typedef struct {
const char *name; /* header */
enum src_type type; /* how to fetch */
const char *path; /* relative sysfs file when SRC_FILE */
} column_t;
/* default columns – keep in same order as original tool */
static const column_t available_columns[] = {
{"DEVNAME", SRC_DEVNAME, NULL},
{"MAJ:MIN", SRC_FILE, "dev"},
{"SIZE", SRC_SIZE, "size"},
{"RO", SRC_FILE, "ro"},
{"TYPE", SRC_TYPE, NULL},
{"SCHED", SRC_SCHED, "queue/scheduler"},
{"NR_RQ", SRC_FILE, "queue/nr_requests"},
{"ROT", SRC_FILE, "queue/rotational"},
{"MODEL", SRC_FILE, "device/model"},
{"QDEPTH", SRC_QDEPTH, "device/queue_depth"},
{"WCACHE", SRC_FILE, "queue/write_cache"},
};
static const size_t n_available_columns = sizeof(available_columns)/sizeof(available_columns[0]);
static const char *default_columns[] = {
"DEVNAME", "MAJ:MIN", "SIZE", "TYPE", "SCHED",
"ROT", "MODEL", "QDEPTH", "NR_RQ", "WCACHE" };
static const size_t n_default_columns = sizeof(default_columns)/sizeof(default_columns[0]);
/* ---------------------------------------------------------- */
/* Helpers */
/* ---------------------------------------------------------- */
static int read_trimmed(const char *path, char *buf, size_t len)
{
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd == -1)
return -1;
ssize_t n = read(fd, buf, (ssize_t)len - 1);
close(fd);
if (n < 0)
return -1;
buf[n] = '\0';
/* trim trailing whitespace */
while (n > 0 && (buf[n-1] == '\n' || buf[n-1] == '\r' || buf[n-1]==' ' || buf[n-1]=='\t'))
buf[--n] = '\0';
return 0;
}
static unsigned long long str_to_ull(const char *s, unsigned long long def)
{
if (!s || *s=='\0') return def;
char *end=NULL;
errno = 0;
unsigned long long v = strtoull(s, &end, 10);
if (errno != 0 || end==s)
return def;
return v;
}
static void human_size(unsigned long long bytes, char *out, size_t outlen)
{
const double g = 1024.0*1024.0*1024.0;
double val = (double)bytes / g;
snprintf(out, outlen, "%.1f GiB", val);
}
static bool is_syspath_accessible(const char *base, const char *rel, char *full, size_t full_len)
{
if (snprintf(full, full_len, "%s/%s", base, rel) >= (int)full_len)
return false;
return access(full, R_OK) == 0;
}
static const column_t *find_column(const char *name)
{
for (size_t i=0;i<n_available_columns;i++)
if (strcasecmp(name, available_columns[i].name)==0)
return &available_columns[i];
return NULL;
}
static bool is_nvme(const char *dev)
{
return strncmp(dev, "nvme", 4)==0;
}
static const char *infer_type(const char *dev, const char *devpath)
{
char path[PATH_MAX];
if (is_syspath_accessible(devpath, "partition", path, sizeof(path)))
return is_nvme(dev)? "NVMePart" : "Part";
if (strncmp(dev, "sd", 2)==0 || strncmp(dev, "hd", 2)==0 || strncmp(dev, "vd", 2)==0)
return "Disk";
if (strncmp(dev, "dm-", 3)==0)
return "DM";
if (is_nvme(dev))
return "NVMeDisk";
if (strncmp(dev, "loop", 4)==0)
return "Loop";
return "BlockDev";
}
static void parse_scheduler(const char *raw, char *out, size_t len)
{
const char *l = strchr(raw, '[');
const char *r = l ? strchr(l, ']') : NULL;
if (l && r && r>l+1) {
size_t n = (size_t)(r - l - 1);
if (n >= len) n = len-1;
memcpy(out, l+1, n);
out[n]='\0';
} else {
strncpy(out, raw, len-1);
out[len-1]='\0';
}
}
static int get_qdepth(const char *devpath, char *out, size_t len)
{
char path[PATH_MAX];
if (!is_syspath_accessible(devpath, "device/queue_depth", path, sizeof(path))) {
/* not found */
strncpy(out, VALUE_MISSING, len);
return -1;
}
if (read_trimmed(path, out, len)!=0)
strncpy(out, VALUE_MISSING, len);
return 0;
}
/* ---------------------------------------------------------- */
/* JSON helpers */
/* ---------------------------------------------------------- */
static void json_print_string(const char *s)
{
putchar('"');
for (const unsigned char *p=(const unsigned char*)s; *p; p++) {
switch (*p) {
case '\\': putchar('\\'); putchar('\\'); break;
case '\"': putchar('\\'); putchar('"'); break;
case '\b': putchar('\\'); putchar('b'); break;
case '\f': putchar('\\'); putchar('f'); break;
case '\n': putchar('\\'); putchar('n'); break;
case '\r': putchar('\\'); putchar('r'); break;
case '\t': putchar('\\'); putchar('t'); break;
default:
if (*p < 0x20) {
/* control chars as \u00XX */
printf("\\u%04x", *p);
} else {
putchar(*p);
}
}
}
putchar('"');
}
/* ---------------------------------------------------------- */
/* Device list + filter */
/* ---------------------------------------------------------- */
static regex_t filter_re;
static void compile_filter_regex(void)
{
const char *pat = "(^dm|^loop|^[a-z]+[0-9]+p[0-9]+$|^[a-z]+[0-9]+n[0-9]+p[0-9]+$)";
if (regcomp(&filter_re, pat, REG_NOSUB | REG_EXTENDED | REG_ICASE)!=0) {
fprintf(stderr, "regex compile failed\n");
exit(EXIT_FAILURE);
}
}
static bool filter_device(const char *name)
{
return regexec(&filter_re, name, 0, NULL, 0)==REG_NOMATCH;
}
static char **list_devices(size_t *out_n)
{
DIR *d = opendir(SYSFS_BASE);
if (!d) {
perror("opendir");
exit(EXIT_FAILURE);
}
size_t cap=32, n=0;
char **list = calloc(cap, sizeof(char*));
if (!list) {
perror("calloc"); exit(EXIT_FAILURE);
}
struct dirent *de;
while ((de = readdir(d))!=NULL) {
if (de->d_name[0]=='.') continue;
if (!filter_device(de->d_name)) continue;
if (n==cap) {
cap*=2;
char **tmp = realloc(list, cap*sizeof(char*));
if(!tmp){perror("realloc"); exit(EXIT_FAILURE);} list=tmp;
}
list[n++] = strdup(de->d_name);
}
closedir(d);
*out_n=n;
return list;
}
/* ---------------------------------------------------------- */
/* Column selection parsing */
/* ---------------------------------------------------------- */
typedef struct {
const column_t *col;
} selcol_t;
static selcol_t selected[MAX_COLUMNS];
static size_t n_selected = 0;
static void add_select(const column_t *c)
{
if (!c) return;
for (size_t i=0;i<n_selected;i++)
if (selected[i].col == c) return; /* unique */
if (n_selected>=MAX_COLUMNS) return;
selected[n_selected++].col = c;
}
static void init_default_selection(void)
{
for (size_t i=0;i<n_default_columns;i++) {
const column_t *c = find_column(default_columns[i]);
if (c) add_select(c);
}
}
static void parse_columns_list(char **argv, int argc)
{
for (int i=0;i<argc;i++) {
const column_t *c = find_column(argv[i]);
if (!c) {
fprintf(stderr, "Invalid column: %s\n", argv[i]);
exit(EXIT_FAILURE);
}
add_select(c);
}
}
static void add_columns_from_csv(const char *csv)
{
char *dup = strdup(csv);
if (!dup) exit(EXIT_FAILURE);
char *tok, *save=NULL;
for (tok=strtok_r(dup, ",", &save); tok; tok=strtok_r(NULL, ",", &save)) {
while (isspace((unsigned char)*tok)) tok++;
if (*tok=='\0') continue;
const column_t *c = find_column(tok);
if (!c) {
fprintf(stderr, "Invalid column in --add: %s\n", tok);
exit(EXIT_FAILURE);
}
add_select(c);
}
free(dup);
}
/* ---------------------------------------------------------- */
/* Value retrieval */
/* ---------------------------------------------------------- */
static void fetch_value(const column_t *col, const char *dev, const char *devpath,
bool verbose, char *out, size_t len)
{
char path[PATH_MAX];
(void)verbose;
switch (col->type) {
case SRC_DEVNAME:
strncpy(out, dev, len-1); out[len-1]='\0';
break;
case SRC_TYPE:
strncpy(out, infer_type(dev, devpath), len-1); out[len-1]='\0';
break;
case SRC_FILE:
if (is_syspath_accessible(devpath, col->path, path, sizeof(path)) &&
read_trimmed(path, out, len)==0)
;
else strncpy(out, VALUE_MISSING, len);
break;
case SRC_SIZE: {
if (is_syspath_accessible(devpath, col->path, path, sizeof(path))) {
char tmp[MAX_VAL_LEN];
if (read_trimmed(path, tmp, sizeof(tmp))==0) {
unsigned long long blocks = str_to_ull(tmp, 0ULL);
unsigned long long bytes = blocks * SECTOR_SIZE;
human_size(bytes, out, len);
break;
}
}
strncpy(out, VALUE_MISSING, len);
break; }
case SRC_SCHED: {
char tmp[MAX_VAL_LEN];
if (is_syspath_accessible(devpath, col->path, path, sizeof(path)) &&
read_trimmed(path, tmp, sizeof(tmp))==0) {
parse_scheduler(tmp, out, len);
} else {
strncpy(out, VALUE_MISSING, len);
}
break; }
case SRC_QDEPTH: {
if (is_nvme(dev)) {
strncpy(out, VALUE_MISSING, len);
} else {
if (get_qdepth(devpath, out, len)!=0)
strncpy(out, VALUE_MISSING, len);
}
break; }
default:
strncpy(out, VALUE_MISSING, len);
}
}
/* ---------------------------------------------------------- */
/* Printing helpers */
/* ---------------------------------------------------------- */
static void print_header(void)
{
for (size_t i=0;i<n_selected;i++) {
printf("%s%s", selected[i].col->name,
(i==n_selected-1)? "\n" : " ");
}
}
static void print_devices_table(char **devlist, size_t n_dev, bool verbose)
{
(void)verbose; /* placeholder: verbose path printing not implemented */
/* first compute column widths */
size_t *w = calloc(n_selected, sizeof(size_t));
if (!w) exit(EXIT_FAILURE);
/* init with header width */
for (size_t i=0;i<n_selected;i++) w[i] = strlen(selected[i].col->name);
/* collect all values into memory */
char ***table = calloc(n_dev, sizeof(char**));
if (!table) exit(EXIT_FAILURE);
for (size_t d=0; d<n_dev; d++) {
const char *dev = devlist[d];
char devpath[PATH_MAX];
snprintf(devpath, sizeof(devpath), "%s/%s", SYSFS_BASE, dev);
table[d] = calloc(n_selected, sizeof(char*));
if (!table[d]) exit(EXIT_FAILURE);
for (size_t c=0;c<n_selected;c++) {
char buf[MAX_VAL_LEN];
fetch_value(selected[c].col, dev, devpath, verbose, buf, sizeof(buf));
table[d][c] = strdup(buf);
size_t l = strlen(buf);
if (l > w[c]) w[c] = l;
}
}
/* print header */
print_header();
/* print rows */
for (size_t d=0; d<n_dev; d++) {
for (size_t c=0;c<n_selected;c++) {
printf("%*s%s", (int)w[c], table[d][c], (c==n_selected-1)? "\n" : " ");
free(table[d][c]);
}
free(table[d]);
}
free(table);
free(w);
}
static void print_devices_json(char **devlist, size_t n_dev, bool verbose)
{
(void)verbose;
putchar('[');
for (size_t d=0; d<n_dev; d++) {
if (d) putchar(',');
putchar('\n');
printf(" {");
const char *dev = devlist[d];
char devpath[PATH_MAX];
snprintf(devpath, sizeof(devpath), "%s/%s", SYSFS_BASE, dev);
for (size_t c=0;c<n_selected;c++) {
char buf[MAX_VAL_LEN];
fetch_value(selected[c].col, dev, devpath, verbose, buf, sizeof(buf));
if (c) printf(", ");
json_print_string(selected[c].col->name); putchar(':'); json_print_string(buf);
}
printf(" }");
}
printf("\n]\n");
}
/* ---------------------------------------------------------- */
/* Main / CLI */
/* ---------------------------------------------------------- */
static void list_available(void)
{
for (size_t i=0;i<n_available_columns;i++)
printf("%s\n", available_columns[i].name);
}
static void usage(FILE *f)
{
fprintf(f,
"lsds – list Linux block devices (modern C)\n\n"
"Options:\n"
" -c, --columns COL1 COL2 … set exact column list (overrides default)\n"
" -a, --add COL1,COL2… add columns to default list\n"
" -l, --list list available column names and exit\n"
" -v, --verbose verbose (not used yet)\n"
" -p, --pivot pivot output (not implemented)\n"
" -r, --realpath show real paths (not implemented)\n"
" -j, --json output in JSON format\n"
" -h, --help show this help\n");
}
int main(int argc, char **argv)
{
bool verbose=false;
bool json_output=false;
/* long options */
static const struct option long_opts[] = {
{"columns", required_argument, 0, 'c'},
{"add", required_argument, 0, 'a'},
{"list", no_argument, 0, 'l'},
{"verbose", no_argument, 0, 'v'},
{"pivot", no_argument, 0, 'p'},
{"realpath",no_argument, 0, 'r'},
{"json", no_argument, 0, 'j'},
{"help", no_argument, 0, 'h'},
{0,0,0,0}
};
init_default_selection();
compile_filter_regex();
int opt;
while ((opt = getopt_long(argc, argv, "c:a:lvprjh", long_opts, NULL)) != -1) {
switch (opt) {
case 'c':
n_selected=0;
parse_columns_list(&argv[optind-1], 1); /* first token already here */
/* parse rest until next option */
while (optind<argc && argv[optind][0]!='-') {
parse_columns_list(&argv[optind], 1);
optind++;
}
break;
case 'a':
add_columns_from_csv(optarg);
break;
case 'l':
list_available();
return 0;
case 'v': verbose=true; break;
case 'p': /* pivot not implemented */ break;
case 'r': /* realpath not implemented */ break;
case 'j': json_output=true; break;
case 'h': usage(stdout); return 0;
default:
usage(stderr); return 1;
}
}
size_t n_dev;
char **devlist = list_devices(&n_dev);
if (n_dev==0) {
fprintf(stderr, "No block devices found.\n");
return 1;
}
if (json_output)
print_devices_json(devlist, n_dev, verbose);
else
print_devices_table(devlist, n_dev, verbose);
/* cleanup */
for (size_t i=0;i<n_dev;i++) free(devlist[i]);
free(devlist);
regfree(&filter_re);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment