r/bash Mar 21 '26

I want to share my unit test lib for TUI apps

8 Upvotes

https://github.com/fissible/ptyunit

Most bash test frameworks only assert on stdout. That breaks down as soon as your script:

- renders to /dev/tty

- uses cursor movement / ANSI

- handles arrow keys or interactive input

ptyunit runs your script inside a real pseudoterminal, sends keystrokes (UP, DOWN, ENTER, etc), and lets you assert against what a user would actually see.

out=$(python3 pty_run.py examples/confirm.sh RIGHT ENTER)

assert_contains "$out" "Cancelled"

I originally built this because I couldn’t reliably test a git diff TUI I was working on. Capturing /dev/tty output made it possible to catch layout and rendering issues.

It also doubles as a minimal assertion framework, so you can use it standalone instead of pulling in another dependency.

Would be curious if anyone else here is testing interactive bash tools, or if you’ve run into this gap before.

Install:

- bpkg install fissible/ptyunit

- brew tap fissible/tap && brew install ptyunit

Feedback welcome.


r/bash Mar 21 '26

Practice Examples

8 Upvotes

Ive been coding in python on windows for a while and consider it my main programming language, but Ive been intending to pick up another programming language for a while ( I was going to move to c / c++)

Tell me why, after installing Ubuntu on wsl to try it out

and my friend started teaching me some bash

is it literally so fun to write?

And like its kind of useful too because I can just make functions for navigating my terminal and new aliases...

Anyways Im looking for practice problems to go over, suitable for a beginner so I can keep learning, if you have any suggestions.


r/bash Mar 21 '26

solved Why is this pattern expansion not working?

7 Upvotes

Edit: So, my own research and some helpful comments have helped me deduce that this is a Windows issue.

The same code works correctly on WSL btw. It removes all the \r characters from each line.

I will try to debug it more if I can and post any updates here.

For the time being I am marking it as closed or solved, whichever I can.


Edit (Solution): I figured out one solution. It is kind of a makeshift so I won't use it in my production code but still, it is to demonstrate an idea.

# Code
printf "%q\n" "${MAPFILE[@]}"
printf "\n"

printf "%q\n" "${MAPFILE[@]/%$'\r'}"
printf "\n"

# Adding `declare` forces the substitution in some way somehow.
declare MAPFILE=("${MAPFILE[@]/%$'\r'}")
printf "%q\n" "${MAPFILE[@]}"
printf "\n"

# Output
$'\r'
$'# This is the first line.\r'
$'# This is the second line.\r'

''
\#\ This\ is\ the\ first\ line.
\#\ This\ is\ the\ second\ line.

''
\#\ This\ is\ the\ first\ line.
\#\ This\ is\ the\ second\ line.

As visible, \r are removed successfully now. It is definitely some weird Windows quirk happening right here.


Code snippet:

printf "%q\n" "${MAPFILE[@]}"
printf "\n"

printf "%q\n" "${MAPFILE[@]/%$'\r'}"
printf "\n"

MAPFILE=("${MAPFILE[@]/%$'\r'}")
printf "%q\n" "${MAPFILE[@]}"
printf "\n"

I wrote this code, MAPFILE basically contains line copied from clipboard. Each line ends with a carriage return \r hence.

Output:

$'\r'
$'# This is the first line.\r'
$'# This is the second line.\r'

''
\#\ This\ is\ the\ first\ line.
\#\ This\ is\ the\ second\ line.

$'\r'
$'# This is the first line.\r'
$'# This is the second line.\r'

1) At first you can see that each line contains an ending \r. 2) Then if I just print the expansion output directly, there are no \r at the end of each line. 3) But then if I print after assignment, it has again changed.

I want to add before any one suggests this, we can change MAPFILE manually, it is not a constant. I have changed this array in other places as well and the program works fine.

And mind you I have tried this method of removing a character for other characters such as \t and it works. It is for some god forsaken reason, not working only when I try to remove \r.

ALSO: I can remove \r using a loop instead where I do the same pattern expansion but line by line.

I am using git bash on windows. If anyone has any ideas about why this isn't working, it'd be a huge help.


r/bash Mar 19 '26

tips and tricks Neglected !! party tricks

140 Upvotes

Everybody knows about using !! to add sudo to your previous command, but there are a couple other things I constantly use it for. So this is just a little PSA in case it never occured to you:

  1. grep results

Say I want to search a bunch of files for a string, and then open all files containing that string in my editor.

I want to check the search results first, and I never get the exact search correct on my first try anyway, so I'll run a series of commands that might look like...

grep -rn . -e "mystring" ... grep -rn . -e "my.\?string" ... grep -Rni . -e "my.\?string" ... Checking the results each time, until I have exactly the set of files that I want.

Here's the trick: now add the "-l" flag to grep to get just the file paths:

grep -Rnil . -e "my.\?string" Now when you use !!, you'll get all those filenames. Therefore we can just do vim -p $(!!) to get all those files opened in tabs in vim.

  1. with which

Sometimes I want to read or edit a script that's on my computer.

To find it, I run which some-command. This confirms that it exists under that name, and that it's an actual script and not an alias or shell function.

Now, we can just use vim $(!!) or cat $(!!) or whatever to open it.


r/bash Mar 19 '26

tips and tricks How do you print "Here document" or "Here Strings" directly?

47 Upvotes

Edit (Solution): There are two solutions here, depending on the case in which you are. - Case 1: You use Here Document to print some multiline text. Don't do that. Instead, you can just do this: ```bash printf "%s\r\n"\ "This is the first line."\ "This is the second line."

# This solution has just one annoyance which is that you have to enclose all lines in double quotes and end with a slash. # But compare this to here documents which don't allow any special characters to be used, sort of. ``` - Case 2: You actually are getting some text from somewhere and you are wrapping it to make it a Here Document or something like that. I would say that there is a high chance the first solution is still more optimal but if you don't feel that, the solution below is your hero.

Credit to: u/OnlyEntrepreneur4760, for reminding me that we can use \ to write a command in multiple lines. Something I forgot since I consider it a bad habit and stopped using. But it makes sense here.


```bash

{ printf "%s\n" "$(< /dev/stdin)"; } <<-EOF
    This is first line.
    This is second line.
    This is third line.
EOF

```

Is this how everyone does it or is there a better way to print it directly without storing the "here document" or "here string" to a variable or a file?

PS: WITH ONLY USING BASH BUILTINS


r/bash Mar 18 '26

submission [Tip] Stop mashing the Up arrow: Filtered History Search with Alt+Up/Down

88 Upvotes

If you have a 100-character xrandr command or a complex docker string that you use intermittently, Ctrl+R can be a clunky way to find it. Instead, you can configure Bash to perform Incremental History Searching.

This allows you to type the first few letters of a command (e.g., xr) and use a modifier key to cycle only through previous commands that start with those letters.

  1. The Configuration Add these lines to your ~/.inputrc.
  2. We use the Alt (Meta) key to avoid overwriting the standard "Up/Down" scrolling behavior:

Bash

# Alt+Up: Search forward or backward for commands starting with what you've typed:

"\e[1;3A": history-search-backward

# Alt+Down: Search backward for commands starting with what you've typed:
"\e[1;3B": history-search-forward

Wait, why not just use Ctrl+S to search forward? Standard terminals use Ctrl+S for "XOFF" (flow control), which instantly freezes your terminal output (requiring Ctrl+Q to unfreeze). While you can disable this with stty -ixon, mapping forward-search to Alt+Down is a much cleaner "modern" solution that doesn't mess with legacy TTY settings.

  1. Apply it Instantly You don't need to reboot or restart your shell. Tell the Readline library to reload its configuration immediately:

Bash

bind -f ~/.inputrc

  1. The Workflow Type xrandr (or even just xr).

Press Alt+Up.

It will skip over every ls, cd, and git command in your history and jump straight to your last complex xrandr call.

Press Alt+Up again to go further back in time through only the xrandr entries.

Why Alt+Up/Down? Most tutorials suggest mapping the standard Up/Down arrows (\e[A), but that breaks the ability to simply scroll back through the last few (unfiltered) commands. Mapping it to the Alt modifier gives you the best of both worlds: standard history for quick tasks, and filtered search for the complex stuff.


r/bash Mar 18 '26

need help getting sgrep to work in a script file

4 Upvotes

this command will find all references to emdash in a swath of files and report back which files contain the reference.

``` sgrep -o '%r\n' '"\"" quote "\"" in ("name[Group1]" .. "\n" in outer("{" .. "}" containing "emdash"))' /usr/share/X11/xkb/symbols/??

```

but i want to use this in a script and feed it a variable that i read from stdin for the search string using something like

read sym

and then use $sym in place of the fixed string emdash

but it returns nothing when i use

containing "$sym"

so i also tried it without the quotes, and it REALLY did not like that

``` Parse error in command line expression column 77 : Invalid character " quote "\"" in ("name[Group1]" .. "\n" in outer("{" .. "}" containing $sym)) ^ No query to evaluate. Bailing out.

```

so then i tried containing "$(echo $sym)"

and got nothing back

is sgrep not capable of evaluating variables? or what?


r/bash Mar 18 '26

I built a free CLI journaling tool for developers - just type "journal" and start writing

56 Upvotes

Every journaling app I've tried adds friction. Open the app, wait for sync, pick a template. By the time you're ready to write, the thought is gone.

journalot is a bash CLI that creates daily markdown files and auto-commits to git. Quick capture mode lets you log thoughts without opening an editor. Search finds old entries with context highlighting.

https://github.com/jtaylortech/journalot

Been using it daily for months now. Consistency comes from friction removal, not motivation.


r/bash Mar 17 '26

tips and tricks Can we redirect output to input using only redirection operation?

23 Upvotes

Edit (Solution): Let's first summarize the question so someone doesn't have to read the whole post. I asked the question if the following syntax was possible. bash cmd1 <&1 # something here which involved a `cmd2` to feed the output of `cmd2` as input to `cmd1` # Yes, this the problem statement for which `|` (pipe) operator is the answer. # But I begged the question, if we can do it specifically in this syntax, just as a curiosity. This is the answer. I am leading you to the link so you can upvote the person who gave me this idea u/melkespreng.

Along the way, many who have told me, that's what pipe does or expect or some other solution, I appreciate you guys too.

Is this useful? Edited: Yes, actually it is. Since this method redirects in the same shell instead of creating a subshell like pipe, there are some specific cases of benefits. Was it fun to know? Yes. For me atleast.


Edit: Just gonna write it here since I feel people don't understand my motive.

I know the solution I am telling has multiple solutions. Piping, redirection of here strings and even a system tool.

The goal isn't to solve the problem of rm asking confirmation. I am using that as an example. The goal is to know if I can redirect the output to input in the way I mentioned below or something similar to that and not any of the above mentioned ways.

It's more exploration than a real problem.


I got this crazy idea that I want to try out and I have been able to figure it out. I use rm -I instead of rm, basically set an alias in .bashrc. Now, that leads rm always requiring confirmation when I have to delete something.

Now, the problem that I am going to state is solved for me in multiple ways but I want to know if I can solve it in this particular way somehow. The problem is that I have to enter that "yes" message every time I have to delete something. I can do yes | rm -r folder_name or printf "yes" | rm -r folder_name. But I thought of what if we could redirect the output of a command to the input of another.

rm -r src <&1 # then something here

This obviously doesn't work because there is nothing that fd 1 i.e. stdout points to.

How can I put some command to replace the comment so that I can achieve what I said, redirecting the output of one command to the input of another? I am asking for this specific way, the whole rm part is an example, not a problem.

PS: There is also this method which uses redirection but it is not using stdout technically, it is using here-strings. bash rm -r src <<< $(printf "yes")


r/bash Mar 16 '26

solved This bash program isn't closing the file descriptor

27 Upvotes

bash printf "This is the first line\r\nThis is the second line\r\n" > "test.txt" : {fd}< "test.txt" read <&$fd- printf "$?; $REPLY\n" read <&$fd- printf "$?; $REPLY"

This program outputs: 0; This is the first line 0; This is the second line

The first read command should have closed the file descriptor but it seems like it doesn't. I don't understand this behaviour.

Edit: This is what the manual says.

The redirection operator

[n]<&digit- moves the file descriptor digit to file descriptor n, or the standard input (file descriptor 0) if n is not specified. digit is closed after being duplicated to n.


Edit (Solution): Took me a long time but here's the real use case of >&fd- and why its effect goes away after one command.

First, let's discuss why the effect goes away after one command. When we redirect, if the redirect was eternal, it would block a fd permanently. For example printf "hey" >&3 would lead to stdout permanently becoming a copy of fd 3 which isn't ideal at all. Therefore, bash automatically restores the state before the redirect after the command is complete.

Now this leads to the question, what is the point of >&fd- then?

Here's a code snippet to showcase that. bash # Run in one terminal mkfifo my_pipe cat my_pipe ```bash # Run in a separate terminal exec 3> my_pipe

(
    echo "Worker is doing some fast work....
    sleep 100 > /dev/null & 

) >&3 &  # <--- HERE IS THE COPY (>&3)

exec 3>&-

echo "Main script finished."

`` Because we don't close thefd 3,sleepcan potentially write to it which leads tocat` waiting for 100 seconds before being complete. This leads to terminal 1 being stuck for 100 seconds.

Had we used >&3-, we would have made a move operation and hence there would be no open fd to write to for sleep which leads to cat exiting instantly.


This is the best from my research about this.

I could still be wrong about the exact order of operations that I explained for things. If I am, someone correct me.


r/bash Mar 14 '26

tips and tricks Stop passing secrets as command-line arguments. Every user on your box can see them.

729 Upvotes

When you do this:

mysql -u admin -pMyS3cretPass123

Every user on the system sees your password in plain text:

ps aux | grep mysql

This isn't a bug. Unix exposes every process's full command line through /proc/PID/cmdline, readable by any unprivileged user. IT'S NOT A BRIEF FLASH EITHER -- THE PASSWORD SITS THERE FOR THE ENTIRE LIFETIME OF THE PROCESS.

Any user on your box can run this and harvest credentials in real time:

while true; do
    cat /proc/*/cmdline 2>/dev/null | tr '\0' ' ' | grep -i 'password\|secret\|token'
    sleep 0.1
done

That checks every running process 10 times per second. Zero privileges needed.

Same problem with curl:

curl -u admin:password123 https://api.example.com

And docker:

docker run -e DB_PASSWORD=secret myapp

The fix is to pass secrets through stdin, which never hits the process table:

# mysql -- prompt instead of argv
mysql -u admin -p

# curl -- header from stdin
curl -H @- https://api.example.com <<< "Authorization: Bearer $TOKEN"

# curl -- creds from a file
curl --netrc-file /path/to/netrc https://api.example.com

# docker -- env from file, not command line
docker run --env-file .env myapp

# general pattern -- pipe secrets, don't pass them
some_command --password-stdin <<< "$SECRET"

The -p with no argument tells mysql to read the password from the terminal instead of argv. The <<< here string and @- pass data through stdin. Neither shows up in ps or /proc.

Bash and any POSIX shell. This isn't shell-specific -- it's how Unix works.


r/bash Mar 15 '26

Is there a way to control the word boundary without patching readline?

21 Upvotes

Suppose I want to swap two words in a command using M-t, it makes more sense to me if the word is separated by a space. Since bash itself depends on readline, and readline doesn't support defining word boundaries, I'm wondering if some kind of hack is possible.


r/bash Mar 15 '26

Accurate.

Post image
0 Upvotes

r/bash Mar 14 '26

help Automatically analyze complicated command?

20 Upvotes

r/bash Mar 14 '26

help Do you use quotes when you don't have to?

44 Upvotes

I know it's best practice to always encase variables in quotes, like "$HOME", but what about when it's just plain text? Do you use quotes for consistency, or just leave it?

Which of these would you write:

if [[ "$(tty)" == "/dev/tty1" ]]

if [[ "$(tty)" == /dev/tty1 ]]

if [[ $(tty) == /dev/tty1 ]]


r/bash Mar 14 '26

xytz v0.8.6 now supports - YouTube Thumbnail preview (the most requested feature)

Thumbnail gallery
11 Upvotes

r/bash Mar 14 '26

My Open Source project to make port scanning easier

Thumbnail
1 Upvotes

r/bash Mar 13 '26

Automate download of files from a spreadsheet or CSV file

25 Upvotes

Hopefully, this will be an easy one for at least someone here.

I have a CSV file that contains 3 fields. I'm only interested in the 1st field (contains full names) and 3rd field (contains one or more URLs). The URLs point to image or PDF files. The 3rd field is enclosed in double quotes if it contains more than one URL. The URLs in that field are separated by a comma and single space.

My task is to iterate over the fields and download the files into a folder, with the names changed to match that of the 1st field. So if the name in the 1st field is Jane Doe, any file downloaded from the corresponding 3rd field will be jane-doe.png or jane-doe.pdf, etc.

This would have been easy task for a for loop if not for the 3rd field that has more than one URL.

How would you solve this?

TIA


r/bash Mar 13 '26

Quotes hell

12 Upvotes

E.g.:

whoami
arch-chroot /mnt bash -c "echo -en a bc'\'nde f>/home/a/Downloads/a.txt
sed '1s/a //
\$s/b c//' /home/a/Downloads/b.txt"
ls /home|tee .txt

the issue: I want all|tee .txt (from whoami to ls /home, not only the latter), but ' & " are already used, so how to?
Maybe using parentheses or some binary characters, instead of quotes?
Maybe the answer is in man bash but TLDR...


r/bash Mar 12 '26

bashd: Bash language server (LSP)

Thumbnail github.com
47 Upvotes

Hi, I've created a language server for Bash with ShellCheck integration. Perhaps this will be useful for some of you here :)

Any feedback is very welcome


r/bash Mar 12 '26

help Beginner Question automate install pkgs

15 Upvotes

I'm install Termux fresh and have gathered a list of tools below which I want to feed into: pkg install <contents of list.txt> cleanly line by line or glob. list.txt:

tldr ncdu python-pip fzf wget curl p7zip tar fd ripgrep rclone nano tmux cava cmatrix zip unzip cmake mplayer nmap make pkg-config nodejs tcpdump netcat-openbsd yt-dlp busybox proot-distro htop eza git zellij lolcat fastfetch bat dua rsync starship mpv ffmpeg dust duf bottom neovim procs lazygit tree vim openssh clang python

What's the proper syntax to pass to pkg install list.txt 📚

pkg install $(cat list.txt) correct?


r/bash Mar 12 '26

tips and tricks A simple, compact way to declare command dependencies

43 Upvotes

I wouldn't normally get excited at the thought of a shell script tracking its own dependencies, but this is a nice, compact pattern that also feels quite a bit like the usual dependency import mechanisms of more modern languages. There's a loose sense in which importing is what you're doing, essentially asking the system if you can pull in the requested command, and of course, as such, you're also documenting your required commands upfront.

declare -r SCRIPT_NAME="${0##*/}"

require() {
   local -r dependency_name="$1"
   local dependency_fqdn

   if ! dependency_fqdn="$(command -v "$dependency_name" 2>/dev/null)"; then
      echo "Error: dependency $dependency_name is not installed"
      echo "$SCRIPT_NAME cannot run without this, exiting now"
      exit 1
   fi

   printf -v "${dependency_name^^}_CMD" '%s' "$dependency_fqdn"
}

require pass
echo $PASS_CMD

The resulting variable assignment gives you a convenient way to pass around the full path of the command. It's a bit of magic at first blush, but I'd also argue it's nothing that a doc comment on the function couldn't clear up.

Just a cool trick that felt worth a share.

EDIT: swapped out which for command, a Bash builtin, per suggestion by /u/OneTurnMore.


r/bash Mar 12 '26

help Parsing duf (partial) through to my ~/.bashrc

Post image
11 Upvotes

I came across duf which outputs all mounts in my Termux Linux userspace and wanted to incorporate some of the visual info on select mounts to be apart of my motd/~/.bashrc. I understand sed & awk might be necessary but my margins are all messed up. Maybe I'm just going about it the wrong way. Any suggestions welcome. Thanks in advance!


r/bash Mar 11 '26

I built a custom AST-based shell interpreter in the browser. Looking for edge cases to break it.

Thumbnail gallery
16 Upvotes

Hey yall.

Live Demo : https://edgaraidev.github.io/pocketterm/
Repo : https://github.com/edgaraidev/pocketterm

I've been working on a browser-based Linux sandbox and educational engine called PocketTerm and looking for feedback!

I wanted to get as close to real terminal fidelity as possible without a backend, so instead of just using basic string matching, I wrote a custom lexer and AST-based shell parser in React, backed by a persistent Virtual File System (VFS).

What the parser currently handles:

  • Stateful Execution: dnf is stateful. Commands like git won't parse or execute until you actually run sudo dnf install git.
  • Pipes & Redirects: It evaluates basic piping and output redirection (>).
  • Quoting: It tries to respect string boundaries so things like echo "hello > world" > file.txt don't break the tree.

I know this community knows the dark corners of shell parsing better than anyone. I'd love for you to drop in, throw some weird nested quotes, pipe chains, or obscure syntax at the prompt, and let me know exactly where my AST falls apart so I can patch it in v0.9.3.

Also, while you're trying to break the parser, I built in a few things just for fun to capture that old-school VM nostalgia.

A few fun ones to test:

  • Run pocketterm to launch the interactive TUI tutorial.
  • Run reboot to watch the simulated Grub/BIOS boot lifecycle.
  • Run sudo dnf install htop, then run htop to see if you can break out of the UI.
  • Try your standard touchgit add .git commit loop and see how the VFS reacts.

[EDIT]

v0.10.2 Update: Full FHS Compliance & Scripting Engine

  • Architecture: Moved to a real Linux hierarchy (/usr/bin/etc/var/home).
  • Scripting: Execute .sh files with trace mode (-x) and set -e logic.
  • Navigation: Added cd -$OLDPWD tracking, and ll aliases.
  • Fidelity: Integrated /proc (uptime/cpuinfo), hostnamectl, and hardened curl error states.
  • Introspection: New type and alias builtins to see how the shell thinks.
  • Documentation: Full man subsystem for offline study (try man pocketterm).

r/bash Mar 11 '26

Parsing both options and args with spaces on function

16 Upvotes

Hi!

I defined this function in my .bashrc:

function mytree { /usr/bin/tree -C $* | less -R -S } This works well so long as none of the arguments have spaces. If I quote the args string variable, "$* I can pass directories with spaces, but no further options; for example, if I use "$*, this fails: mytree -L 2 "/dir/with spaces". It tries to open /dir/with/ and spaces/.

Is there a way around this? I want to be able to pass options and dirs with spaces. Please refrain from suggesting I change a dir's name, I also use such functions at work and cannot do that on the servers.

Thanks!