← BackBash30 Q

Bash

30 questions | entry to principal | answers written to be said out loud, not read

entry

What is a shell, really?

A shell is a program that reads commands I type, or lines from a script, and asks the operating system to run them. It handles parsing, expansion, piping processes together, and reporting back results. Bash is one specific shell, and the terminal is just the window I'm typing into, not the shell itself.

What does the shebang line at the top of a script actually do?

The shebang, like the env bash line at the very top, tells the kernel which interpreter to hand the rest of the file to when it's executed directly. I use env bash instead of a hardcoded bin bash path because it looks up bash on the current PATH, which matters across machines where bash lives in different places. Without a shebang and execute permission, the file just runs in whatever shell invoked it.

How do exit codes and the dollar-question-mark variable work?

Every command returns an exit code when it finishes, 0 for success and anything from 1 to 255 for some kind of failure, and the meaning of a nonzero code is defined by that specific program. The question-mark variable holds the exit code of the last command that ran, so I check it immediately before running anything else that would overwrite it. In scripts I use that to branch on success or failure, or to propagate a meaningful code back to whatever called the script.

How does PATH and the environment work, and how do you check if a command exists?

PATH is a colon-separated list of directories the shell searches in order to resolve a bare command name to an executable. Environment variables are inherited by child processes when they're exported, while a plain shell variable stays local to the current shell. To check whether a command is available before relying on it, I use command -v rather than which, because command -v is a builtin and works consistently across shells.

What is command substitution?

Command substitution runs a command and swaps in its standard output as text, trimming trailing newlines. I always use the dollar-paren form over backticks because it's easier to read and nests cleanly without escaping. It's how I capture things like the current git branch or a timestamp into a variable for later use.

How do functions work in bash?

A function groups commands under a name I can call like any other command. Arguments come in positionally inside the function, just like script arguments, and an at-sign expansion holds all of them. A function's exit status is the exit status of its last command unless I return an explicit code, and return only accepts numbers 0 to 255.

What loop constructs does bash give you?

for iterates over a list of words, a range, or array elements. while runs as long as a condition stays true, which is what I reach for when reading lines from a file or a command's output. until is the inverse of while, running until the condition becomes true. For reading files line by line I use a while loop with read -r, because that avoids word splitting and preserves backslashes in each line.

What do cut, sort, uniq, and tr each give you?

cut pulls out columns by delimiter or fixed character position when I don't need awk's full power. sort orders lines, and uniq collapses adjacent duplicate lines, so I almost always pipe through sort first since uniq only catches duplicates that are next to each other. tr does simple character-level translation or deletion, like squashing repeated whitespace or converting a file to lowercase. They're small, but chaining them together covers a surprising amount of everyday text reshaping.

How do you use find, and how do file permissions and chmod work?

find walks a directory tree and filters by name, type, size, modification time, or a lot else, and I pair it with -exec or xargs to act on the results. Permissions are read, write, and execute for owner, group, and others, and chmod changes them either symbolically, like adding execute for the owner, or numerically, like setting 755. I use the numeric form when I want an exact, unambiguous permission set, especially in scripts.

junior

What's the difference between bash, sh, and zsh?

sh is the original, minimal POSIX shell interface, and on most systems today it's a symlink to something else like dash or bash running in a restricted mode. bash is the GNU shell with more features, arrays, and better string handling, and it's the default login shell on most Linux boxes. zsh is macOS's default since Catalina and adds things like better completion and globbing, but for portable scripts I still target POSIX sh or explicitly bash with a bash shebang.

How do single quotes, double quotes, and no quotes differ in bash?

Single quotes preserve everything literally, no variable expansion and no word splitting, which is what I reach for by default. Double quotes still expand variables and command substitution but protect the result from word splitting and globbing, so a double-quoted variable stays as one argument even if its value has spaces. Leaving something unquoted lets the shell split it on whitespace and expand any glob characters, which is almost never what I actually want for a variable holding a path or filename.

How does parameter expansion and setting default values work?

Parameter expansion is everything you can do inside the curly-brace form beyond a plain variable name: string length, substring removal, substitution, and defaults. The colon-dash default form gives me a fallback value if a variable is unset or empty without changing it, while the colon-equals form actually assigns it. I use the colon-question form when I want the script to fail loudly with a clear message if something required was never set.

How do bash arrays work?

Bash has indexed arrays for ordered lists and associative arrays for key-value pairs once I declare them with declare -A. I build one with parentheses, loop over the array quoted so each element survives spaces intact, and get the count with a length expansion. They're bash-specific, not POSIX sh, so if a script needs to run under plain sh I have to redesign around them.

Why do you declare local inside functions?

Without local, every variable a function sets is global by default, which means one function can silently stomp on a variable another part of the script is using. I mark anything scoped to the function as local so it only exists for that call and disappears when the function returns. It's one of the cheapest habits for avoiding hard-to-trace bugs in longer scripts.

What's the difference between single-bracket and double-bracket tests in bash?

A single bracket is the POSIX test command, it's actually a program, so it needs quoting discipline and doesn't understand things like logical and or regex matching directly. A double bracket is a bash keyword with more forgiving parsing, safer handling of unquoted variables, and support for pattern and regex matching. In bash scripts specifically I default to double brackets for readability and safety, and only drop to a single bracket when I need a script to run under plain POSIX sh.

When do you reach for a case statement instead of if elif?

case matches a value against a series of patterns, including globs, and it reads a lot cleaner than a long chain of elif checks when I'm branching on something like a command-line argument or an environment name. Each pattern ends in a double semicolon, and I always include a star catch-all branch so an unexpected value doesn't silently fall through with no action taken.

How does redirection work, and what's the difference between stdout, stderr, and combining them?

File descriptor 1 is stdout, 2 is stderr, and by default both go to the terminal. A single angle bracket redirects stdout to a file, overwriting it, and a double angle bracket appends. Redirecting stderr into wherever stdout currently points only works if the order is right, so I write it after the stdout redirect if I want both streams landing in the same file. A here-doc feeds multi-line input to a command, and a here-string feeds it a single value without needing a temp file.

How do grep, sed, and awk divide the work in text processing?

grep finds lines matching a pattern and is what I reach for first when I just need to know if or where something appears. sed does line-oriented find and replace or deletion, which is my tool for quick, scripted edits to a stream of text. awk is closer to a small programming language built around fields and records, so when I need to pull out a column, sum a value, or do anything with structure across fields, awk is the right level of tool instead of stretching sed past what it's good at.

How do jq and curl fit into a bash script that talks to an API?

curl makes the HTTP request, and I always pin down the method, headers, and how I want failures surfaced, using the fail flag so a 4xx or 5xx exits nonzero instead of printing an error page and continuing. jq then parses and filters the JSON response, pulling out exactly the field I need instead of grepping JSON as if it were plain text, which breaks the moment formatting changes. Together they let a shell script consume a real API almost like a small client would.

How do tar, rsync, and ssh work together for moving data around?

tar packages a directory tree into a single archive, usually piped through gzip, so I can move or store a whole folder as one file. rsync syncs files between locations and only transfers the parts that actually changed, which makes it far more efficient than copying everything every time, and it can run over ssh to a remote host. ssh itself is how I run commands on a remote machine or tunnel a connection, and combining it with rsync or a piped tar is the backbone of most of my backup and deploy scripts.

senior

Why do you start scripts with set -euo pipefail?

set -e stops the script the moment a command fails instead of plowing ahead with bad state. set -u treats an unset variable as an error instead of silently expanding to an empty string, which catches typos in variable names. pipefail makes a pipeline fail if any command in it fails, not just the last one. I treat this as close to a mandatory header for any real script, with the understanding that set -e has edge cases around conditionals and command substitution I still have to think about.

What are word splitting and globbing, and why do they bite people?

Word splitting is bash breaking an unquoted expansion into separate words on spaces, tabs, and newlines from IFS. Globbing is the shell expanding patterns like a star dot txt into matching filenames before the command ever sees the literal asterisk. The classic bug is looping over an unquoted variable of filenames, where a filename with a space gets split into two arguments, or a glob that matches nothing gets passed through literally. Quoting variables and being deliberate about globs is how I avoid both.

What's the difference between a subshell and the current shell?

A subshell is a forked copy of the current shell that inherits variables and functions but can't change the parent's state, anything it does to variables or the working directory disappears when it exits. Parentheses, a pipeline segment, and command substitution all run in subshells. I hit this constantly when a loop after a pipe can't update a variable in the outer script, because the loop is actually running in its own subshell.

What are signals and traps used for?

A signal is the kernel's way of interrupting a process, like SIGINT when I hit Ctrl+C or SIGTERM when something asks a process to shut down. trap lets a script register a handler that runs when a signal arrives, which I use to clean up temp files, kill background jobs, or print a clear message before the script actually exits. A trap on EXIT is especially useful because it fires on any exit path, normal or not.

How do background jobs and wait work?

Appending an ampersand runs a command in the background and immediately gives control back to the shell, and jobs lists what's running while the bang variable holds the PID of the most recently backgrounded process. wait blocks until a background job finishes, or until a specific PID does if I pass it one, which is how I fan work out in parallel and then collect it before moving on. I still have to handle each job's exit status individually if I care whether any of them actually failed.

How do pipes work, and why does pipefail matter?

A pipe connects one command's stdout to the next command's stdin, and every command in the pipeline actually runs concurrently, not one after another. Without pipefail, the exit status of a pipeline is just the exit status of the last command, so grep failing to find a match in the middle of a pipeline can be silently swallowed. With pipefail set, the pipeline's exit status is nonzero if any stage fails, which is what I want when set -e needs to catch a broken step.

What do xargs and process substitution let you do?

xargs takes lines from stdin and builds them into arguments for another command, which matters because a lot of commands, like rm or cp, don't read filenames from stdin themselves. I pair xargs with null-separated find output to handle filenames with spaces or newlines safely. Process substitution lets a command's output be treated like a file, which is handy for diffing the output of two commands without writing temp files first.

staff

Cron vs systemd timers, when do you pick one over the other?

Cron is simple and universally understood, a line in crontab with a schedule and a command, and it's fine for straightforward periodic jobs. systemd timers pair a timer unit with a service unit, and they give me dependency ordering, better logging through journalctl, the ability to catch up on a missed run, and resource controls that cron doesn't have. On a modern Linux server I default to a systemd timer for anything that matters, and reach for cron mainly for quick, low-stakes scheduling.

How do you handle temp files safely, and what makes a script idempotent?

I create temp files and directories with mktemp instead of hardcoding a name in tmp, because mktemp guarantees a unique, unpredictable path and avoids a race where two runs, or an attacker, could collide on the same filename. I pair that with a trap on EXIT so the temp path always gets cleaned up. Idempotent means I can run the script twice and get the same end state both times, so I check before creating, use flags like mkdir -p instead of failing on an existing directory, and design steps so a retry doesn't duplicate work or corrupt state.

principal

What does shellcheck catch, and when should you stop writing bash and switch to Python?

shellcheck is a static analyzer for shell scripts that flags unquoted variables, unsafe word splitting, common quoting mistakes, and portability issues between sh and bash, and I run it on every script before it ships. I switch away from bash once a script needs real data structures beyond flat arrays, proper error handling with exceptions, unit tests, or any nontrivial string and JSON manipulation, because at that point Python is more maintainable and less fragile than fighting bash's quoting rules. Bash stays great for orchestration and gluing other commands together, it's a poor fit for actual application logic.

Fast recall

shebang = picks the interpreter | $? = last exit code | set -e = stop on error | pipefail = catch pipeline failures | [[ ]] = bash test, safer parsing | $(...) = command substitution | IFS = word split characters | trap = run on exit or signal | mktemp = safe temp file path | xargs = stdin into arguments | rsync = sync only the changes | jq = parse and filter JSON | command -v = check a command exists | shellcheck = lint your script

BH·Bash·github.com/bunlongheng/study