Last active
May 12, 2025 08:47
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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