ps aux written in bash without forking
github.com
github.com
> # this one function looks simple but it took so fucking long
Heh. I can't count how many times I've written a column-aligning functions in various programming languages, and each time it is a pain. And it sounds simple in my head - just get max-length of each column, and add spaces up to the next multiple of tab-size.
But even in Python with f-strings and all the fancy padding stuff it has, it is ends up a convoluted, unreadable mess:
# randomwordgenerator.com
table = [
['agony', 'kick', 'pump'],
['frown', 'lonely', 'mutation'],
['sail', 'tasty', 'want'],
]
tab_width = 4
n_rows, n_cols = len(table), len(table[0])
max_width = [
max(len(table[r][c]) for r in range(n_rows))
for c in range(n_cols)
]
for r in range(n_rows):
for c in range(n_cols):
item = table[r][c]
if c == n_cols - 1:
# do not print space after last item
print(item)
else:
# only print newline after last item
# EDIT: found a bug here after commenting...
# width = int(((max_width[c] + 1) / tab_width) * tab_width)
width = ((max_width[c] // tab_width) + 1) * tab_width
print(f'{item:<{width}}', end='')
Even while writing this code for the comment for the hundredth time, I've had to fix at least 5 bugs! Truly horrible.Surely there's a library out there to do this job, it seems like such a common use-case. I'm surprised it's not in the standard library to be honest!
I agree it would be very nice to have this in the standard library.
[1] https://pypi.org/project/tabulate/, https://github.com/astanin/python-tabulate
https://metacpan.org/pod/Term::Table or https://metacpan.org/pod/Text::Table for simple table formatting.
https://stackoverflow.com/questions/10865483/print-results-i...
sql = "SELECT * FROM someTable"
cursor.execute(sql)
conn.commit()
results = cursor.fetchall()
widths = []
columns = []
tavnit = '|'
separator = '+'
for cd in cursor.description:
widths.append(max(cd[2], len(cd[0])))
columns.append(cd[0])
for w in widths:
tavnit += " %-"+"%ss |" % (w,)
separator += '-'*w + '--+'
print(separator)
print(tavnit % tuple(columns))
print(separator)
for row in results:
print(tavnit % row)
print(separator)
Am I missing anything, are there any glaring bugs? I don't see this as a particularly difficult problem. widths = [len(max(col, key=len)) for col in zip(*table)]
print("\n".join(" | ".join([f"{r:<{w}}" for r, w in zip(row, widths)]) for row in table))
# Gives:
# agony | kick | pump
# frown | lonely | mutation
# sail | tasty | wantI personally prefer readable to compact, so this is what I ended up with, inspired by your version:
tab_width = 4
columns = zip(*table)
column_widths = [
max(len(item) for item in column)
for column in columns
]
column_indents = [
tab_width * ((width // tab_width) + 1)
for width in column_widths
]
for row in table:
items = [
f'{item:<{indent}}'
for item, indent in zip(row, column_indents)
]
print(''.join(items).rstrip()) widths = [max(map(len, col)) for col in zip(*table)]
print("\n".join(" | ".join(f"{r:<{w}}" for r, w in zip(row, widths)) for row in table))Maybe you and I can make a pact here and now to just not column-align data, but rather use some simpler human-readable format? Win-win?
To be fair, most of scripts that print tabular data for human reading also contain "--json-out" flag.
Maybe I can just use YAML as a compromise? :)
> Have you ever used the Octopus Deploy command line tool?
Thankfully, no :) but I've had similar experience with other pieces of software. Some even provide ability to output JSON, but you need to find the right incantation to do so (looking at you, Docker CLI, and your --format="{{json .}}"!)
on format_tab x do
w:max each r in x count @ r end
f:(list "%%-%is ")format -1 drop w
("\n",""fuse f,"%s")format x
end
The basic idea is to assemble a format string which right-pads all but the last column, and then use that format string uniformly across all rows.And then in K for comparison (somewhat clumsily):
ft:{,/"\n"/((1+|/#:''t)$/:t:-1_'x),'-1#'x}
Much simpler still if we permit trailing whitespace padding: ft:{,/"\n"/(1+|/#:''x)$/:x} max_width = max(len(x) for y in table for x in y))
items = []
for row in table:
rowstr = "".join(
el + " " * (max_width - len(el)) for el in row
)
items.append(rowstr)
# or, to print inline
#for el in row:
# print(item, end = " " * (max_width - len(el)))
#print()
print("\n".join(items)) from itertools import zip_longest
tab_width = 4
col_max_widths = [(v := max(map(len, a))) + tab_width - (v % tab_width)
for a in zip_longest(*table, fillvalue='')]
for row in table:
print(''.join(c.ljust(cw) for cw, c in zip(col_max_widths, row)))Also that code (IMHO, of course) still gives me a headache when I try to mentally parse it.
The challenge here is how to get ps in an environment where the command won't run. And in that case, awk won't run either.
As for why it might happen, well just save the following shell script and run it on a Linux system.
#! /bin/sh
$0 &
perl -e 'push @big, 1 while 1` &
$0
Now that your system is struggling, figure out how to rescue it.(True story. I once worked with a careless programmer who would make mistakes whose results looked like that fairly regularly. It was...an education.)
> What if you're ssh'd into a machine, you're in your trusty bash shell, but unfortunately you cannot spawn any new processes because literally all other pids are taken. What do you do?
I'd look in the /proc/[pid]/ filesystem for visibility into what processes are exhausting the PID space.
`kill` is a shell builtin in bash, you don't have to rely on forking a new process like /bin/kill. If you can find out the parent process whose children are exhausting PIDs you're well on your way to stopping it and getting a handle on things again.
And I'll be darned, this script parses /proc. No | pipes or $( .. ) substitutions that would need to spawn another bash subshell process either. Pretty clean.
This went over quite well.
"that's disgusting"
"this has got to stop"
"you've gone too far with this"
"is this a joke?"
"I never knew the c could stand for Cthulhu."
And after the exec, if they asked me to parse a Python expression, I'd type "eval(expr)".
Edit: also, all valid JSON is valid Python. Do not `eval(input_data)` in prod or I will haunt you. But, in an emergency…
My example was just as a joke.
For real use, I would only use it with my own trusted input.
Sure, you could fake it with custom objects and all of that, but not when I'm pasting a string value into a REPL. If you had hijacked my workstation, shell or the remote python to the point you can exploit that... Yeah. I don't think you'd need me as a user then anymore.
ps(){ (cd /proc;for i in [0-9]*;do echo $i: $(tr '\0' ' ' < $i/cmdline);done); }
ps() { for i in /proc/[0-9]*; do readarray -d '' -t cmdline < "$i/cmdline"; printf "%s: %s\n" "${i#/proc/}" "${cmdline[*]}"; done; }From the source code:
# so initially i was hoping you could get everything from /proc/<pid>/status
# because it's easy to parse (in most cases) but apparently you can't get
# things like the cpu% :(Realistically you can get a useful `ps` by catting/grepping `/proc/<pid>/status` for all the processes, but the goal here was to replicate exactly the output of procps `ps aux`. Except for the bugs in column alignment, she fixed those intentionally.
[[ $cmdline ]] && exec {cmdline}>&-
and exec {cmdline}< "$dir"/cmdline || continue
work?"The redirection operator:
[n]>&word
"shall duplicate one output file descriptor from another, or shall close one. If word evaluates to one or more digits, the file descriptor denoted by n, or standard output if n is not specified, shall be made to be a copy of the file descriptor denoted by word; if the digits in word do not represent a file descriptor already open for output, a redirection error shall result; see Consequences of Shell Errors. If word evaluates to '-', file descriptor n, or standard output if n is not specified, is closed. Attempts to close a file descriptor that is not open shall not constitute an error. If word evaluates to something else, the behavior is unspecified."https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V...
cd /proc
echo *$
even if it were an issue.. say on a terminal without working scrollback.. you can just as easily:
echo 1*
and so forth.Break it into tenths (ninths, maybe, with no leading zeroes?), or finer granularity if necessary.
The argument list isn't nearly as constrained as it was a decade ago. "echo {00000001..10000000}" works in bash on most modern distros where shells on earlier systems would have choked on a tiny ARG_MAX.
Second idea was `sudo reboot now`
you can `exec sudo` but this will hose you if it tries to fork (because now you've lost your bash).
But I think a prerequisite is that you already have a root shell; some systems don't allow accessing all of /proc unless you're root, and if you figure out what process is exhausting all your pids and want to kill it, you probably need to be root to do that, unless you're very lucky and that process happens to be running under your regular user account.
At any rate, you'd need to `exec restart now`, because just `restart now` would try to fork. (Also, there's no `restart` command; I think you meant `reboot`, and it doesn't need arguments. `shutdown -r now` would also do it.)
I quickly realized I couldn't do it (because I did not, at the time, know where and how exactly socket information was held in /proc/), but I offered to write trimmed-down replacements of `ps` and `fuser` instead. The interviewer deemed that - and my solutions eventually produced in that wretched, browser-based word processor - acceptable, and a few weeks later, I shipped out to the on-site interview series.
Now I wonder if the hypothetical scenario presented as the motivation for this exercise is more grounded in reality than Izabera (thanks for all your help in #bash over years btw!) would care to admit... ;)
A long ago for fun I created an interactive website to explore this type of scenario. https://oops.cmdchallenge.com
"echo *" doesn't list ALL files in the directory. "echo .* *" does. set -O dotglobI love all those gurus. They've taught me so much over the past decade.
But what if i'm instead in my trusty POSIX shell without bash support? The bash script is not POSIX complient :(
$ ./psaux.bash
./psaux.bash: line 182: printf: `(': invalid format character
./psaux.bash: line 185: printf: `(': invalid format character
$ rpm -q bash
bash-3.2-33.el5_11.4.0.1> You can freely copy and use this in any interview in which you get asked that question. No other uses are ever allowed.
Arch-TK: I live with her
isabera: stares intensely
ps faux
does it for me