From 33284944c618a04eccfb9645123bb2dd7eaa64b5 Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:10:50 +0000 Subject: [PATCH 1/8] feat: add -J/-j option parsing for JSON output Add Fjson/Fjsonl global flags, option parsing for -J (nested JSON) and -j (JSON Lines), mutual exclusivity validation, and default field selection when -F is not explicitly given. --- lib/common.h | 3 ++ src/main.c | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/store.c | 3 ++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/lib/common.h b/lib/common.h index 1e6c67ee..d5beb43e 100644 --- a/lib/common.h +++ b/lib/common.h @@ -684,6 +684,9 @@ extern int ErrStat; extern uid_t Euid; extern int Fcntx; extern int Ffield; +extern int Fjson; +extern int Fjsonl; +extern int Fjson_first_proc; extern int Ffilesys; extern int Fhelp; extern int Fhost; diff --git a/src/main.c b/src/main.c index 0c2e98a0..199ffe1d 100644 --- a/src/main.c +++ b/src/main.c @@ -151,7 +151,7 @@ int main(int argc, char *argv[]) { * Create option mask. */ (void)snpf(options, sizeof(options), - "?a%sbc:%sD:d:%s%sf:F:g:hHi:%s%slL:%s%snNo:Op:QPr:%ss:S:tT:u:" + "?a%sbc:%sD:d:%s%sf:F:g:hHi:%s%sJjlL:%s%snNo:Op:QPr:%ss:S:tT:u:" "UvVwx:%s%s%s", #if defined(HAS_AFS) && defined(HASAOPT) @@ -556,6 +556,24 @@ int main(int argc, char *argv[]) { case '?': Fhelp = 1; break; + case 'J': + if (GOp == '+') { + (void)fprintf(stderr, "%s: +J is not supported\n", Pn); + err = 1; + break; + } + Fjson = 1; + Ffield = 1; + break; + case 'j': + if (GOp == '+') { + (void)fprintf(stderr, "%s: +j is not supported\n", Pn); + err = 1; + break; + } + Fjsonl = 1; + Ffield = 1; + break; case 'i': if (!GOv || *GOv == '-' || *GOv == '+') { Fnet = 1; @@ -1097,6 +1115,14 @@ int main(int argc, char *argv[]) { (void)fprintf(stderr, "%s: -x must accompany +d or +D\n", Pn); err++; } + if (Fjson && Fjsonl) { + (void)fprintf(stderr, "%s: -J and -j are mutually exclusive\n", Pn); + err++; + } + if ((Fjson || Fjsonl) && Fterse) { + (void)fprintf(stderr, "%s: -J/-j and -t are mutually exclusive\n", Pn); + err++; + } #if defined(HASEOPT) if (Efsysl) { @@ -1126,6 +1152,58 @@ int main(int argc, char *argv[]) { } #endif /* defined(HASEOPT) */ + /* + * If -J/-j was given, ensure field selections are set. + * If -F was also given with field chars, those selections are already + * in FieldSel[]. Otherwise, enable the default field set. + */ + if (Fjson || Fjsonl) { + int has_fields = 0; + for (i = 0; FieldSel[i].nm; i++) { + if (FieldSel[i].st && FieldSel[i].id != LSOF_FID_PID && + FieldSel[i].id != LSOF_FID_MARK) { + has_fields = 1; + break; + } + } + if (!has_fields) { + for (i = 0; FieldSel[i].nm; i++) { +#if !defined(HASPPID) + if (FieldSel[i].id == LSOF_FID_PPID) + continue; +#endif +#if !defined(HASTASKS) + if (FieldSel[i].id == LSOF_FID_TCMD || + FieldSel[i].id == LSOF_FID_TID) + continue; +#endif +#if !defined(HASFSTRUCT) + if (FieldSel[i].id == LSOF_FID_CT || + FieldSel[i].id == LSOF_FID_FA || + FieldSel[i].id == LSOF_FID_FG || + FieldSel[i].id == LSOF_FID_NI) + continue; +#endif +#if defined(HASSELINUX) + if ((FieldSel[i].id == LSOF_FID_CNTX) && !CntxStatus) + continue; +#else + if (FieldSel[i].id == LSOF_FID_CNTX) + continue; +#endif + if (FieldSel[i].id == LSOF_FID_RDEV) + continue; +#if !defined(HASZONES) + if (FieldSel[i].id == LSOF_FID_ZONE) + continue; +#endif + FieldSel[i].st = 1; + if (FieldSel[i].opt && FieldSel[i].ov) + *(FieldSel[i].opt) |= FieldSel[i].ov; + } + } + } + if (DChelp || err || Fhelp || fh || version) usage(ctx, err ? 1 : 0, fh, version); /* diff --git a/src/store.c b/src/store.c index 8e7dfd25..a51c49cf 100644 --- a/src/store.c +++ b/src/store.c @@ -72,6 +72,9 @@ int NcacheReload = 1; /* 1 == call ncache_load() */ #endif /* defined(HASNCACHE) */ int Ffield = 0; /* -f and -F status */ +int Fjson = 0; /* -J JSON output status */ +int Fjsonl = 0; /* -j JSON Lines output status */ +int Fjson_first_proc = 1; /* first process flag for JSON comma handling */ int FgColW; /* FILE-FLAG column width */ int Fhelp = 0; /* -h option status */ int Fhost = 1; /* -H option status */ From f221123f9228c2128a2860d2834a340f634ba7be Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:13:54 +0000 Subject: [PATCH 2/8] feat: add JSON string escaping and output helpers --- src/print.c | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/print.c b/src/print.c index 494f9eb3..ed8e0b56 100644 --- a/src/print.c +++ b/src/print.c @@ -31,6 +31,7 @@ #include "common.h" #include "cli.h" #include "proto.h" +#include /* * Local definitions, structures and function prototypes @@ -93,6 +94,83 @@ static char *lkup_svcnam(struct lsof_context *ctx, int h, int p, int pr, static int printinaddr(struct lsof_context *ctx); static int human_readable_size(SZOFFTYPE sz, int print, int col); +/* + * JSON output helpers + */ + +static void json_puts_escaped(const char *s) { + const unsigned char *p = (const unsigned char *)s; + while (*p) { + switch (*p) { + case '"': + fputs("\\\"", stdout); + break; + case '\\': + fputs("\\\\", stdout); + break; + case '\b': + fputs("\\b", stdout); + break; + case '\f': + fputs("\\f", stdout); + break; + case '\n': + fputs("\\n", stdout); + break; + case '\r': + fputs("\\r", stdout); + break; + case '\t': + fputs("\\t", stdout); + break; + default: + if (*p < 0x20) + printf("\\u%04x", (unsigned int)*p); + else + putchar(*p); + break; + } + p++; + } +} + +static void json_print_str(int *sep, const char *key, const char *val) { + if (*sep) + putchar(','); + printf("\"%s\":\"", key); + json_puts_escaped(val); + putchar('"'); + *sep = 1; +} + +static void json_print_int(int *sep, const char *key, int val) { + if (*sep) + putchar(','); + printf("\"%s\":%d", key, val); + *sep = 1; +} + +static void json_print_uint64_str(int *sep, const char *key, uint64_t val) { + if (*sep) + putchar(','); + printf("\"%s\":\"%" PRIu64 "\"", key, val); + *sep = 1; +} + +static void json_print_long(int *sep, const char *key, long val) { + if (*sep) + putchar(','); + printf("\"%s\":%ld", key, val); + *sep = 1; +} + +static void json_print_ulong(int *sep, const char *key, unsigned long val) { + if (*sep) + putchar(','); + printf("\"%s\":%lu", key, val); + *sep = 1; +} + #if !defined(HASNORPC_H) /* * fill_portmap() -- fill the RPC portmap program name table via a conversation From cf0b102b50914214e5af562751fa6c25b609c01b Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:21:29 +0000 Subject: [PATCH 3/8] feat: implement JSON process/file printing for -J/-j --- src/print.c | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) diff --git a/src/print.c b/src/print.c index ed8e0b56..29ca3ad5 100644 --- a/src/print.c +++ b/src/print.c @@ -1865,6 +1865,180 @@ int human_readable_size(SZOFFTYPE sz, int print, int col) { return strlen(buf); } +/* + * json_print_file() - print a single file entry as JSON fields + */ +static void json_print_file(struct lsof_context *ctx, int *sep) { + char buf[128]; + char fd_str[FDLEN]; + char type[TYPEL]; + char *cp; + unsigned long ul; + + if (FieldSel[LSOF_FIX_FD].st) { + if (Lf->fd_type == LSOF_FD_NUMERIC) { + char num_buf[32]; + snprintf(num_buf, sizeof(num_buf), "%d", Lf->fd_num); + json_print_str(sep, "fd", num_buf); + } else { + fd_to_string(Lf->fd_type, Lf->fd_num, fd_str); + json_print_str(sep, "fd", fd_str); + } + } + if (FieldSel[LSOF_FIX_ACCESS].st) { + char a[2] = {access_to_char(Lf->access), '\0'}; + if (a[0] != ' ') + json_print_str(sep, "access", a); + } + if (FieldSel[LSOF_FIX_LOCK].st) { + char l[2] = {lock_to_char(Lf->lock), '\0'}; + if (l[0] != ' ') + json_print_str(sep, "lock", l); + } + if (FieldSel[LSOF_FIX_TYPE].st && Lf->type != LSOF_FILE_NONE) { + file_type_to_string(Lf->type, Lf->unknown_file_type_number, type, + TYPEL); + json_print_str(sep, "type", type); + } + if (FieldSel[LSOF_FIX_DEVCH].st && Lf->dev_ch && Lf->dev_ch[0]) { + for (cp = Lf->dev_ch; *cp == ' '; cp++) + ; + if (*cp) + json_print_str(sep, "device", cp); + } + if (FieldSel[LSOF_FIX_DEVN].st && Lf->dev_def) { + if (sizeof(unsigned long) > sizeof(dev_t)) + ul = (unsigned long)((unsigned int)Lf->dev); + else + ul = (unsigned long)Lf->dev; + snprintf(buf, sizeof(buf), "0x%lx", ul); + json_print_str(sep, "device_number", buf); + } + if (FieldSel[LSOF_FIX_RDEV].st && Lf->rdev_def) { + if (sizeof(unsigned long) > sizeof(dev_t)) + ul = (unsigned long)((unsigned int)Lf->rdev); + else + ul = (unsigned long)Lf->rdev; + snprintf(buf, sizeof(buf), "0x%lx", ul); + json_print_str(sep, "raw_device", buf); + } + if (FieldSel[LSOF_FIX_SIZE].st && Lf->sz_def) { + json_print_uint64_str(sep, "size", (uint64_t)Lf->sz); + } + if (FieldSel[LSOF_FIX_OFFSET].st && Lf->off_def) { + json_print_uint64_str(sep, "offset", (uint64_t)Lf->off); + } + if (FieldSel[LSOF_FIX_INODE].st && Lf->inp_ty == 1) { + json_print_uint64_str(sep, "inode", (uint64_t)Lf->inode); + } + if (FieldSel[LSOF_FIX_NLINK].st && Lf->nlink_def) { + json_print_long(sep, "nlink", Lf->nlink); + } + if (FieldSel[LSOF_FIX_PROTO].st && Lf->inp_ty == 2) { + for (cp = Lf->iproto; *cp == ' '; cp++) + ; + if (*cp) + json_print_str(sep, "protocol", cp); + } + +#if defined(HASFSTRUCT) + if (FieldSel[LSOF_FIX_FG].st && (Fsv & FSV_FG) && (Lf->fsv & FSV_FG) && + (FsvFlagX || Lf->ffg || Lf->pof)) { + json_print_str(sep, "flags", print_fflags(ctx, Lf->ffg, Lf->pof)); + } +#endif /* defined(HASFSTRUCT) */ + + if (FieldSel[LSOF_FIX_NAME].st && Lf->nm) { + json_print_str(sep, "name", Lf->nm); + } + + /* TCP/TPI info as nested object */ + if (Lf->lts.type >= 0 && FieldSel[LSOF_FIX_TCPTPI].st) { + if (*sep) + putchar(','); + printf("\"tcp_info\":{"); + int tsep = 0; + + if ((Ftcptpi & TCPTPI_STATE) && Lf->lts.type == 0) { + if (!TcpNstates) + (void)build_IPstates(ctx); + int s = Lf->lts.state.i; + if (s >= 0 && s < TcpNstates && TcpSt[s]) + json_print_str(&tsep, "state", TcpSt[s]); + else { + snprintf(buf, sizeof(buf), "UNKNOWN_%d", s); + json_print_str(&tsep, "state", buf); + } + } +#if defined(HASTCPTPIQ) + if (Ftcptpi & TCPTPI_QUEUES) { + if (Lf->lts.rqs) + json_print_ulong(&tsep, "recv_queue", Lf->lts.rq); + if (Lf->lts.sqs) + json_print_ulong(&tsep, "send_queue", Lf->lts.sq); + } +#endif /* defined(HASTCPTPIQ) */ +#if defined(HASTCPTPIW) + if (Ftcptpi & TCPTPI_WINDOWS) { + if (Lf->lts.rws) + json_print_ulong(&tsep, "read_window", Lf->lts.rw); + if (Lf->lts.wws) + json_print_ulong(&tsep, "write_window", Lf->lts.ww); + } +#endif /* defined(HASTCPTPIW) */ + putchar('}'); + *sep = 1; + } +} + +/* + * json_print_proc_fields() - print process-level JSON fields + */ +static void json_print_proc_fields(struct lsof_context *ctx, int *sep) { + int ty; + char *cp; + + if (FieldSel[LSOF_FIX_PID].st) + json_print_int(sep, "pid", Lp->pid); + +#if defined(HASTASKS) + if (FieldSel[LSOF_FIX_TID].st && Lp->tid) + json_print_int(sep, "tid", Lp->tid); + if (FieldSel[LSOF_FIX_TCMD].st && Lp->tcmd) + json_print_str(sep, "task_cmd", Lp->tcmd); +#endif + +#if defined(HASZONES) + if (FieldSel[LSOF_FIX_ZONE].st && Fzone && Lp->zn) + json_print_str(sep, "zone", Lp->zn); +#endif + +#if defined(HASSELINUX) + if (FieldSel[LSOF_FIX_CNTX].st && Fcntx && Lp->cntx && CntxStatus) + json_print_str(sep, "security_context", Lp->cntx); +#endif + + if (FieldSel[LSOF_FIX_PGID].st && Fpgid) + json_print_int(sep, "pgid", Lp->pgid); + +#if defined(HASPPID) + if (FieldSel[LSOF_FIX_PPID].st && Fppid) + json_print_int(sep, "ppid", Lp->ppid); +#endif + + if (FieldSel[LSOF_FIX_CMD].st) + json_print_str(sep, "command", Lp->cmd ? Lp->cmd : "(unknown)"); + + if (FieldSel[LSOF_FIX_UID].st) + json_print_int(sep, "uid", (int)Lp->uid); + + if (FieldSel[LSOF_FIX_LOGIN].st) { + cp = printuid(ctx, (UID_ARG)Lp->uid, &ty); + if (ty == 0) + json_print_str(sep, "login", cp); + } +} + /* * print_proc() - print process */ @@ -1898,6 +2072,56 @@ int print_proc(struct lsof_context *ctx) { } return (0); } + /* + * JSON output modes — only emit on PrPass 1 (actual printing pass) + */ + if ((Fjson || Fjsonl) && PrPass) { + for (Lf = Lp->file; Lf; Lf = Lf->next) { + if (is_file_sel(ctx, Lp, Lf)) + break; + } + if (!Lf) + return (rv); + rv = 1; + + if (Fjson) { + /* Nested JSON: process object with files array */ + if (!Fjson_first_proc) + putchar(','); + Fjson_first_proc = 0; + int sep = 0; + putchar('{'); + json_print_proc_fields(ctx, &sep); + if (sep) + putchar(','); + printf("\"files\":["); + int first_file = 1; + for (Lf = Lp->file; Lf; Lf = Lf->next) { + if (!is_file_sel(ctx, Lp, Lf)) + continue; + if (!first_file) + putchar(','); + putchar('{'); + int fsep = 0; + json_print_file(ctx, &fsep); + putchar('}'); + first_file = 0; + } + printf("]}"); + } else { + /* JSON Lines: one line per file, process fields denormalized */ + for (Lf = Lp->file; Lf; Lf = Lf->next) { + if (!is_file_sel(ctx, Lp, Lf)) + continue; + int sep = 0; + putchar('{'); + json_print_proc_fields(ctx, &sep); + json_print_file(ctx, &sep); + printf("}\n"); + } + } + return (rv); + } /* * If fields have been selected, output the process-only ones, provided * that some file has also been selected. From 5eba279198f23416f52b1b0f9cc722c248b3404b Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:25:05 +0000 Subject: [PATCH 4/8] feat: add JSON envelope wrapper for -J nested output Emits {"lsof_version":"...","processes":[...]} around process objects. Handles empty results and suppresses -F marker output in JSON modes during repeat cycles. --- src/main.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main.c b/src/main.c index 199ffe1d..20e64a00 100644 --- a/src/main.c +++ b/src/main.c @@ -32,6 +32,7 @@ #include "common.h" #include "cli.h" +#include "version.h" /* * Local definitions @@ -1390,6 +1391,10 @@ int main(int argc, char *argv[]) { (void)qsort((QSORT_P *)slp, (size_t)Nlproc, (size_t)sizeof(struct lproc *), comppid); } + if (Fjson) { + printf("{\"lsof_version\":\"%s\",\"processes\":[", LSOF_VERSION); + Fjson_first_proc = 1; + } if ((n = Nlproc)) { #if defined(HASNCACHE) @@ -1546,6 +1551,11 @@ int main(int argc, char *argv[]) { } Lf = lf; } + if (Fjson) { + printf("]}\n"); + } else if (Fjsonl && RptTm) { + putchar('\n'); + } /* * If a repeat time is set, sleep for the specified time. * @@ -1593,7 +1603,9 @@ int main(int argc, char *argv[]) { } #endif /* defined(HAS_STRFTIME) */ - if (Ffield) { + if (Fjson || Fjsonl) { + /* JSON modes handle their own cycle separation */ + } else if (Ffield) { putchar(LSOF_FID_MARK); #if defined(HAS_STRFTIME) From f6226510f580135ffd1877a849a42aa226bf9b5b Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:27:20 +0000 Subject: [PATCH 5/8] test: add JSON and JSON Lines output test cases --- Makefile.am | 4 +- tests/case-30-json-output.bash | 71 +++++++++++++++++++++++++++++++++ tests/case-31-jsonl-output.bash | 50 +++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100755 tests/case-30-json-output.bash create mode 100755 tests/case-31-jsonl-output.bash diff --git a/Makefile.am b/Makefile.am index fbe28fac..410c6ca3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -132,7 +132,9 @@ DIALECT_NEUTRAL_TESTS = tests/case-00-hello.bash \ tests/case-20-offset-field.bash \ tests/case-20-repeat-count.bash \ tests/case-21-exit-Q-status.bash \ - tests/case-22-empty-process-name.bash + tests/case-22-empty-process-name.bash \ + tests/case-30-json-output.bash \ + tests/case-31-jsonl-output.bash TESTS = $(DIALECT_NEUTRAL_TESTS) EXTRA_DIST += $(DIALECT_NEUTRAL_TESTS) \ tests/case-13-classic.bash \ diff --git a/tests/case-30-json-output.bash b/tests/case-30-json-output.bash new file mode 100755 index 00000000..4ad075bc --- /dev/null +++ b/tests/case-30-json-output.bash @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +source tests/common.bash + +# Skip if python3 not available (needed for JSON validation) +if ! command -v python3 &>/dev/null; then + echo "SKIP: python3 not available" >> $report + exit 77 +fi + +{ + # Test 1: -J produces valid JSON + output=$($lsof -J -p $$ 2>/dev/null) + if ! echo "$output" | python3 -m json.tool > /dev/null 2>&1; then + echo "FAIL: -J output is not valid JSON" >> $report + exit 1 + fi + echo "PASS: -J produces valid JSON" >> $report + + # Test 2: -J output has lsof_version and processes keys + echo "$output" | python3 -c " +import sys, json +d = json.load(sys.stdin) +assert 'lsof_version' in d, 'missing lsof_version' +assert 'processes' in d, 'missing processes' +assert isinstance(d['processes'], list), 'processes is not a list' +assert len(d['processes']) > 0, 'no processes found' +" 2>&1 || { echo "FAIL: -J output structure invalid" >> $report; exit 1; } + echo "PASS: -J output structure valid" >> $report + + # Test 3: -J -Fpcfn only includes selected fields + output_sel=$($lsof -J -Fpcfn -p $$ 2>/dev/null) + echo "$output_sel" | python3 -c " +import sys, json +d = json.load(sys.stdin) +p = d['processes'][0] +assert 'pid' in p, 'missing pid' +assert 'command' in p, 'missing command' +f = p['files'][0] +allowed_file_keys = {'fd', 'name'} +actual_file_keys = set(f.keys()) +extra_keys = actual_file_keys - allowed_file_keys +assert not extra_keys, 'unexpected file keys with -Fpcfn: %s' % extra_keys +assert 'uid' not in p, 'uid should not be present with -Fpcfn' +" 2>&1 || { echo "FAIL: -J field selection not working" >> $report; exit 1; } + echo "PASS: -J field selection works" >> $report + + # Test 4: -J empty result produces valid JSON + output_empty=$($lsof -J -p 999999 2>/dev/null) + echo "$output_empty" | python3 -c " +import sys, json +d = json.load(sys.stdin) +assert d['processes'] == [], 'empty result should have empty processes array' +" 2>&1 || { echo "FAIL: -J empty result is not valid JSON" >> $report; exit 1; } + echo "PASS: -J empty result is valid JSON" >> $report + + # Test 5: -J and -j are mutually exclusive + if $lsof -J -j 2>/dev/null; then + echo "FAIL: -J -j should error" >> $report + exit 1 + fi + echo "PASS: -J -j mutual exclusivity" >> $report + + # Test 6: -J and -t are mutually exclusive + if $lsof -J -t 2>/dev/null; then + echo "FAIL: -J -t should error" >> $report + exit 1 + fi + echo "PASS: -J -t mutual exclusivity" >> $report + + exit 0 +} > $report 2>&1 diff --git a/tests/case-31-jsonl-output.bash b/tests/case-31-jsonl-output.bash new file mode 100755 index 00000000..6a6b23c0 --- /dev/null +++ b/tests/case-31-jsonl-output.bash @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +source tests/common.bash + +# Skip if python3 not available +if ! command -v python3 &>/dev/null; then + echo "SKIP: python3 not available" >> $report + exit 77 +fi + +{ + # Test 1: -j produces valid JSON Lines (each line is valid JSON) + tmpout=$(mktemp) + $lsof -j -p $$ 2>/dev/null > "$tmpout" + fail=0 + while IFS= read -r line; do + if [ -z "$line" ]; then + continue + fi + if ! echo "$line" | python3 -m json.tool > /dev/null 2>&1; then + echo "FAIL: -j line is not valid JSON: $line" >> $report + fail=1 + break + fi + done < "$tmpout" + rm -f "$tmpout" + if [ $fail -ne 0 ]; then + exit 1 + fi + echo "PASS: -j produces valid JSON Lines" >> $report + + # Test 2: -j lines contain both process and file fields (denormalized) + line=$($lsof -j -p $$ 2>/dev/null | head -1) + echo "$line" | python3 -c " +import sys, json +d = json.load(sys.stdin) +assert 'pid' in d, 'missing pid' +assert 'fd' in d or 'command' in d, 'missing file or process fields' +" 2>&1 || { echo "FAIL: -j line missing expected fields" >> $report; exit 1; } + echo "PASS: -j lines have denormalized fields" >> $report + + # Test 3: -j with no matching process produces empty output + output=$($lsof -j -p 999999 2>/dev/null) + if [ -n "$output" ]; then + echo "FAIL: -j with no matches should produce empty output" >> $report + exit 1 + fi + echo "PASS: -j empty result is empty" >> $report + + exit 0 +} > $report 2>&1 From c2e600fa88fa3cbf2c0dba4fee857915343d193a Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:27:52 +0000 Subject: [PATCH 6/8] docs: add -J/-j JSON output flags to man page --- Lsof.8 | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Lsof.8 b/Lsof.8 index 51192e19..1e5fd037 100644 --- a/Lsof.8 +++ b/Lsof.8 @@ -900,6 +900,47 @@ When specified without a PGID set that's all it does. .B \-H directs lsof to print human readable sizes, e.g. 123.4K 456.7M. .TP \w'names'u+4 +.B \-J +selects JSON output mode. Instead of the traditional tabular or +.B \-F +field output, lsof produces a single JSON object on stdout containing +a +.B "processes" +array. Each process object contains its fields and a +.B "files" +array of open file entries. +.IP +Field selection follows the same rules as +.BR \-F : +use +.B \-F +with field characters to select which fields appear in the JSON output. +Without +.BR \-F , +the default field set is used. +.IP +.B \-J +is mutually exclusive with +.B \-j +and +.BR \-t . +Warnings and errors are sent to stderr; stdout is always valid JSON. +.TP \w'names'u+4 +.B \-j +selects JSON Lines output mode. Each open file produces one JSON +object per line, combining process and file fields in a single +denormalized record. This format is suitable for streaming pipelines, +log ingestion (Splunk, Datadog, Elastic), and line\-oriented tools. +.IP +Field selection follows the same rules as +.BR \-J . +.IP +.B \-j +is mutually exclusive with +.B \-J +and +.BR \-t . +.TP \w'names'u+4 .BI \-i " [i]" selects the listing of files any of whose Internet address matches the address specified in \fIi\fP. From 34d6b1b4889221ec4ba0bf88168c8ac608eaff77 Mon Sep 17 00:00:00 2001 From: kurok <22548029+kurok@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:46:58 +0000 Subject: [PATCH 7/8] fix: move JSON envelope to print.c, add version.h build deps Refactors json_open_envelope/json_close_envelope into print.c which already includes version.h via the correct build ordering. Updates all dialect Makefiles to add version.h as a dependency for print.o. --- lib/dialects/aix/Makefile | 2 +- lib/dialects/darwin/Makefile | 2 +- lib/dialects/freebsd/Makefile | 2 +- lib/dialects/hpux/kmem/Makefile | 2 +- lib/dialects/hpux/pstat/Makefile | 2 +- lib/dialects/linux/Makefile | 2 +- lib/dialects/netbsd/Makefile | 2 +- lib/dialects/openbsd/Makefile | 2 +- lib/dialects/osr/Makefile | 2 +- lib/dialects/sun/Makefile | 2 +- lib/dialects/uw/Makefile | 2 +- lib/proto.h | 2 ++ src/main.c | 6 ++---- src/print.c | 16 ++++++++++++++++ 14 files changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/dialects/aix/Makefile b/lib/dialects/aix/Makefile index 980e78f8..00a57915 100644 --- a/lib/dialects/aix/Makefile +++ b/lib/dialects/aix/Makefile @@ -155,7 +155,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/darwin/Makefile b/lib/dialects/darwin/Makefile index 81fab7eb..e7ababc0 100644 --- a/lib/dialects/darwin/Makefile +++ b/lib/dialects/darwin/Makefile @@ -162,7 +162,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/freebsd/Makefile b/lib/dialects/freebsd/Makefile index 33c0aaec..e1e40b18 100644 --- a/lib/dialects/freebsd/Makefile +++ b/lib/dialects/freebsd/Makefile @@ -141,7 +141,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/hpux/kmem/Makefile b/lib/dialects/hpux/kmem/Makefile index 080063da..d159227f 100644 --- a/lib/dialects/hpux/kmem/Makefile +++ b/lib/dialects/hpux/kmem/Makefile @@ -147,7 +147,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/hpux/pstat/Makefile b/lib/dialects/hpux/pstat/Makefile index 9eace673..f30140e1 100644 --- a/lib/dialects/hpux/pstat/Makefile +++ b/lib/dialects/hpux/pstat/Makefile @@ -136,7 +136,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/linux/Makefile b/lib/dialects/linux/Makefile index f8adaa60..cbfefebf 100644 --- a/lib/dialects/linux/Makefile +++ b/lib/dialects/linux/Makefile @@ -167,7 +167,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/netbsd/Makefile b/lib/dialects/netbsd/Makefile index 602673f2..c62f67e5 100644 --- a/lib/dialects/netbsd/Makefile +++ b/lib/dialects/netbsd/Makefile @@ -145,7 +145,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/openbsd/Makefile b/lib/dialects/openbsd/Makefile index 5b566fc0..36ee75c1 100644 --- a/lib/dialects/openbsd/Makefile +++ b/lib/dialects/openbsd/Makefile @@ -145,7 +145,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/osr/Makefile b/lib/dialects/osr/Makefile index 79c38dfd..1b3e1f02 100644 --- a/lib/dialects/osr/Makefile +++ b/lib/dialects/osr/Makefile @@ -151,7 +151,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/sun/Makefile b/lib/dialects/sun/Makefile index c3e0cd4b..449947bc 100644 --- a/lib/dialects/sun/Makefile +++ b/lib/dialects/sun/Makefile @@ -146,7 +146,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/dialects/uw/Makefile b/lib/dialects/uw/Makefile index cf9f38e6..c5fdb1a5 100644 --- a/lib/dialects/uw/Makefile +++ b/lib/dialects/uw/Makefile @@ -145,7 +145,7 @@ misc.o: ${HDR} misc.c node.o: ${HDR} node.c -print.o: ${HDR} print.c +print.o: ${HDR} version.h print.c proc.o: ${HDR} proc.c diff --git a/lib/proto.h b/lib/proto.h index 2caceb80..2d28a90c 100644 --- a/lib/proto.h +++ b/lib/proto.h @@ -187,6 +187,8 @@ extern void print_init(struct lsof_context *ctx); extern void printname(struct lsof_context *ctx, int nl); extern char *print_kptr(KA_T kp, char *buf, size_t bufl); extern int print_proc(struct lsof_context *ctx); +extern void json_open_envelope(void); +extern void json_close_envelope(void); extern void fd_to_string(enum lsof_fd_type fd_type, int fd_num, char *buf); extern void printrawaddr(struct lsof_context *ctx, struct sockaddr *sa); extern void print_tcptpi(struct lsof_context *ctx, int nl); diff --git a/src/main.c b/src/main.c index 20e64a00..cbba9ff7 100644 --- a/src/main.c +++ b/src/main.c @@ -32,7 +32,6 @@ #include "common.h" #include "cli.h" -#include "version.h" /* * Local definitions @@ -1392,8 +1391,7 @@ int main(int argc, char *argv[]) { (size_t)sizeof(struct lproc *), comppid); } if (Fjson) { - printf("{\"lsof_version\":\"%s\",\"processes\":[", LSOF_VERSION); - Fjson_first_proc = 1; + json_open_envelope(); } if ((n = Nlproc)) { @@ -1552,7 +1550,7 @@ int main(int argc, char *argv[]) { Lf = lf; } if (Fjson) { - printf("]}\n"); + json_close_envelope(); } else if (Fjsonl && RptTm) { putchar('\n'); } diff --git a/src/print.c b/src/print.c index 29ca3ad5..bdf2321d 100644 --- a/src/print.c +++ b/src/print.c @@ -31,6 +31,7 @@ #include "common.h" #include "cli.h" #include "proto.h" +#include "version.h" #include /* @@ -1865,6 +1866,21 @@ int human_readable_size(SZOFFTYPE sz, int print, int col) { return strlen(buf); } +/* + * json_open_envelope() - emit JSON opening envelope for -J mode + */ +void json_open_envelope(void) { + printf("{\"lsof_version\":\"%s\",\"processes\":[", LSOF_VERSION); + Fjson_first_proc = 1; +} + +/* + * json_close_envelope() - emit JSON closing envelope for -J mode + */ +void json_close_envelope(void) { + printf("]}\n"); +} + /* * json_print_file() - print a single file entry as JSON fields */ From bb8bab1e26ab1c8548be773307c9a90333f295e3 Mon Sep 17 00:00:00 2001 From: YuriyRyabikov <22548029+kurok@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:17:39 +0000 Subject: [PATCH 8/8] refactor: address code review feedback - Extract select_default_fields() to deduplicate field selection logic shared between -F and -J/-j default handling (jiegec review) - Escape JSON keys via json_print_key() helper (jiegec review) - Add json_print_char() for single-char fields like access and lock, avoiding unnecessary char[2] array construction (jiegec review) --- src/main.c | 136 +++++++++++++++++++++------------------------------- src/print.c | 66 +++++++++++++++---------- 2 files changed, 96 insertions(+), 106 deletions(-) diff --git a/src/main.c b/src/main.c index cbba9ff7..bfcc714b 100644 --- a/src/main.c +++ b/src/main.c @@ -47,6 +47,58 @@ static int GetOpt(struct lsof_context *ctx, int ct, char *opt[], char *rules, int *err); static char *sv_fmt_str(struct lsof_context *ctx, char *f); +/* + * select_default_fields() - enable the default set of FieldSel entries + * + * Shared by -F (with no field chars) and -J/-j (when no -F given). + */ +static void select_default_fields(void) { + int i; + for (i = 0; FieldSel[i].nm; i++) { + +#if !defined(HASPPID) + if (FieldSel[i].id == LSOF_FID_PPID) + continue; +#endif /* !defined(HASPPID) */ + +#if !defined(HASTASKS) + if (FieldSel[i].id == LSOF_FID_TCMD) + continue; +#endif /* !defined(HASTASKS) */ + +#if !defined(HASFSTRUCT) + if (FieldSel[i].id == LSOF_FID_CT || FieldSel[i].id == LSOF_FID_FA || + FieldSel[i].id == LSOF_FID_FG || FieldSel[i].id == LSOF_FID_NI) + continue; +#endif /* !defined(HASFSTRUCT) */ + +#if defined(HASSELINUX) + if ((FieldSel[i].id == LSOF_FID_CNTX) && !CntxStatus) + continue; +#else /* !defined(HASSELINUX) */ + if (FieldSel[i].id == LSOF_FID_CNTX) + continue; +#endif /* !defined(HASSELINUX) */ + + if (FieldSel[i].id == LSOF_FID_RDEV) + continue; /* for compatibility */ + +#if !defined(HASTASKS) + if (FieldSel[i].id == LSOF_FID_TID) + continue; +#endif /* !defined(HASTASKS) */ + +#if !defined(HASZONES) + if (FieldSel[i].id == LSOF_FID_ZONE) + continue; +#endif /* !defined(HASZONES) */ + + FieldSel[i].st = 1; + if (FieldSel[i].opt && FieldSel[i].ov) + *(FieldSel[i].opt) |= FieldSel[i].ov; + } +} + /* * main() - main function for lsof */ @@ -429,51 +481,7 @@ int main(int argc, char *argv[]) { } else if (*GOv == '0') Terminator = '\0'; } - for (i = 0; FieldSel[i].nm; i++) { - -#if !defined(HASPPID) - if (FieldSel[i].id == LSOF_FID_PPID) - continue; -#endif /* !defined(HASPPID) */ - -#if !defined(HASTASKS) - if (FieldSel[i].id == LSOF_FID_TCMD) - continue; -#endif /* !defined(HASTASKS) */ - -#if !defined(HASFSTRUCT) - if (FieldSel[i].id == LSOF_FID_CT || - FieldSel[i].id == LSOF_FID_FA || - FieldSel[i].id == LSOF_FID_FG || - FieldSel[i].id == LSOF_FID_NI) - continue; -#endif /* !defined(HASFSTRUCT) */ - -#if defined(HASSELINUX) - if ((FieldSel[i].id == LSOF_FID_CNTX) && !CntxStatus) - continue; -#else /* !defined(HASSELINUX) */ - if (FieldSel[i].id == LSOF_FID_CNTX) - continue; -#endif /* !defined(HASSELINUX) */ - - if (FieldSel[i].id == LSOF_FID_RDEV) - continue; /* for compatibility */ - -#if !defined(HASTASKS) - if (FieldSel[i].id == LSOF_FID_TID) - continue; -#endif /* !defined(HASTASKS) */ - -#if !defined(HASZONES) - if (FieldSel[i].id == LSOF_FID_ZONE) - continue; -#endif /* !defined(HASZONES) */ - - FieldSel[i].st = 1; - if (FieldSel[i].opt && FieldSel[i].ov) - *(FieldSel[i].opt) |= FieldSel[i].ov; - } + select_default_fields(); #if defined(HASFSTRUCT) Ffield = FsvFlagX = 1; @@ -1166,42 +1174,8 @@ int main(int argc, char *argv[]) { break; } } - if (!has_fields) { - for (i = 0; FieldSel[i].nm; i++) { -#if !defined(HASPPID) - if (FieldSel[i].id == LSOF_FID_PPID) - continue; -#endif -#if !defined(HASTASKS) - if (FieldSel[i].id == LSOF_FID_TCMD || - FieldSel[i].id == LSOF_FID_TID) - continue; -#endif -#if !defined(HASFSTRUCT) - if (FieldSel[i].id == LSOF_FID_CT || - FieldSel[i].id == LSOF_FID_FA || - FieldSel[i].id == LSOF_FID_FG || - FieldSel[i].id == LSOF_FID_NI) - continue; -#endif -#if defined(HASSELINUX) - if ((FieldSel[i].id == LSOF_FID_CNTX) && !CntxStatus) - continue; -#else - if (FieldSel[i].id == LSOF_FID_CNTX) - continue; -#endif - if (FieldSel[i].id == LSOF_FID_RDEV) - continue; -#if !defined(HASZONES) - if (FieldSel[i].id == LSOF_FID_ZONE) - continue; -#endif - FieldSel[i].st = 1; - if (FieldSel[i].opt && FieldSel[i].ov) - *(FieldSel[i].opt) |= FieldSel[i].ov; - } - } + if (!has_fields) + select_default_fields(); } if (DChelp || err || Fhelp || fh || version) diff --git a/src/print.c b/src/print.c index bdf2321d..351ef574 100644 --- a/src/print.c +++ b/src/print.c @@ -135,41 +135,57 @@ static void json_puts_escaped(const char *s) { } } -static void json_print_str(int *sep, const char *key, const char *val) { +/* + * json_print_key() - print comma separator and escaped JSON key + */ +static void json_print_key(int *sep, const char *key) { if (*sep) putchar(','); - printf("\"%s\":\"", key); - json_puts_escaped(val); putchar('"'); + json_puts_escaped(key); + printf("\":"); *sep = 1; } +static void json_print_str(int *sep, const char *key, const char *val) { + json_print_key(sep, key); + putchar('"'); + json_puts_escaped(val); + putchar('"'); +} + +static void json_print_char(int *sep, const char *key, char val) { + json_print_key(sep, key); + putchar('"'); + if (val == '"') + fputs("\\\"", stdout); + else if (val == '\\') + fputs("\\\\", stdout); + else if (val < 0x20) + printf("\\u%04x", (unsigned int)(unsigned char)val); + else + putchar(val); + putchar('"'); +} + static void json_print_int(int *sep, const char *key, int val) { - if (*sep) - putchar(','); - printf("\"%s\":%d", key, val); - *sep = 1; + json_print_key(sep, key); + printf("%d", val); } static void json_print_uint64_str(int *sep, const char *key, uint64_t val) { - if (*sep) - putchar(','); - printf("\"%s\":\"%" PRIu64 "\"", key, val); - *sep = 1; + json_print_key(sep, key); + printf("\"%" PRIu64 "\"", val); } static void json_print_long(int *sep, const char *key, long val) { - if (*sep) - putchar(','); - printf("\"%s\":%ld", key, val); - *sep = 1; + json_print_key(sep, key); + printf("%ld", val); } static void json_print_ulong(int *sep, const char *key, unsigned long val) { - if (*sep) - putchar(','); - printf("\"%s\":%lu", key, val); - *sep = 1; + json_print_key(sep, key); + printf("%lu", val); } #if !defined(HASNORPC_H) @@ -1902,14 +1918,14 @@ static void json_print_file(struct lsof_context *ctx, int *sep) { } } if (FieldSel[LSOF_FIX_ACCESS].st) { - char a[2] = {access_to_char(Lf->access), '\0'}; - if (a[0] != ' ') - json_print_str(sep, "access", a); + char a = access_to_char(Lf->access); + if (a != ' ') + json_print_char(sep, "access", a); } if (FieldSel[LSOF_FIX_LOCK].st) { - char l[2] = {lock_to_char(Lf->lock), '\0'}; - if (l[0] != ' ') - json_print_str(sep, "lock", l); + char l = lock_to_char(Lf->lock); + if (l != ' ') + json_print_char(sep, "lock", l); } if (FieldSel[LSOF_FIX_TYPE].st && Lf->type != LSOF_FILE_NONE) { file_type_to_string(Lf->type, Lf->unknown_file_type_number, type,