In bash, what is the difference between grouping commands with `( ... )` and with `{ ...; }`, and what happens to a `cd` or a `umask` performed inside each form?
answer
- one of them forks, the other does not
- parentheses buy isolation
- directory and umask changes are scoped
- braces need a space and a semicolon
- cd inside parens never moves the script
basics
~20 sParentheses run the group in a forked subshell; braces run it in the current shell. A cd, umask or variable assignment inside parentheses is undone when the group ends, while the same thing inside braces changes the running shell.
solid answer
~50 s`( ... )` forks a subshell and runs the group there, so everything the group changes about shell state — the working directory after `cd`, the file-creation mask after `umask`, variable assignments, `set` options — is discarded when the group exits. `{ ...; }` is pure syntactic grouping: the commands run in the current shell, so those same changes persist. Both forms return the exit status of the last command in the group, and both can be redirected as a unit, which is why they look interchangeable. The syntax differs too: `{` is a reserved word, so it needs whitespace after it and a `;` or newline before the closing `}`, while `(` is an operator and needs neither. In practice you reach for `( cd "$dir" && do-work )` precisely when you want the directory change scoped to that block, and you pay one extra process for it.
code
bash · 8 lines#!/usr/bin/env bash
cd /usr
( cd /tmp; echo "inside parens: $PWD"; x=1 )
echo "after parens: $PWD x='${x-unset}'"
{ cd /tmp; echo "inside braces: $PWD"; y=1; }
echo "after braces: $PWD y='${y-unset}'"go deeper
Know that ( ... ) starts a new shell process while { ...; } does not, and remember the brace syntax rules: space after the opening brace, semicolon or newline before the closing one.
Explain what the fork buys and costs — isolation of working directory, umask, variables, options and traps, in exchange for one extra process and no way to hand a value back up.
Show the idiom in use: wrapping a directory-changing or umask-changing block in ( ... ) so the rest of the script is unaffected, and say where you avoid the fork, such as a loop body that runs thousands of times.
Argue isolation as a design habit — scoping side effects makes a long script's blocks independently reviewable — and set the team's rule for when a block should become a function or leave bash entirely.
## Two things that look like one thing Both forms let you treat several commands as a single unit — redirect them together, put them on one side of `&&`, or apply one `>log` to all of their output. That surface similarity hides the only difference that matters: `( ... )` creates a new process and `{ ...; }` does not. ```bash ( cd /tmp; pwd ) # runs in a child process { cd /tmp; pwd; } # runs right here ``` ## What the fork isolates When bash evaluates `(`, it forks. The child begins life as a duplicate of the current shell: same variables, same working directory, same umask, same open descriptors, same functions. Anything the child then changes belongs to the child alone, and vanishes when it exits: - **Working directory.** `( cd /tmp && tar -cf out.tar . )` leaves the parent exactly where it was. This is the idiom's main use: a block that must operate elsewhere without stranding the rest of the script in the wrong directory. - **umask.** `( umask 077; secret-writing-tool )` tightens the file-creation mask for one block and restores nothing manually, because the tightened mask lived only in the child. - **Variables.** `( x=1 )` then `echo "$x"` in the parent prints nothing. Same reason as the classic piped-loop counter: assignments in a child are invisible upward. - **Shell options and traps.** Options set with `set` inside the group, and traps installed inside it, apply to that child process only. Brace grouping isolates none of this, because there is no second process. `{ cd /tmp; }` really moves the script. ## What the fork does *not* change The subshell is still bash running your script — it is not a new invocation of bash and it does not re-read any startup file. It inherits functions and non-exported shell variables (unlike a child *program*, which only sees exported ones), so `( my_function )` works. The exit status of the group is the exit status of the last command it ran, so `( exit 3 ); echo $?` prints 3. Redirections attach to the group as a whole in both forms: ```bash { echo one; echo two; } > both.txt ( echo one; echo two ) > both.txt ``` And the child cannot send state upward — only bytes on a stream and an exit code. ## The syntax rules people trip on `{` and `}` are reserved words, which means bash only recognises them where a command word can appear: ```bash { echo hi; } # correct { echo hi } # error: 'hi' and '}' become arguments to echo, then bash hits EOF {echo hi; } # error: '{echo' is parsed as one word, a command name ``` So: a space after `{`, and a `;` or newline before `}`. Parentheses are operators, so `(echo hi)` parses fine — though a space is conventional for readability. ## Choosing between them Use `{ ...; }` by default. It is free, and grouping for a shared redirection or a shared `||` is its whole job: ```bash { echo "starting"; run-step; echo "done"; } >> run.log 2>&1 ``` Reach for `( ... )` when you actively want the isolation — a directory change, a tightened umask, an experimental `set -x`, a variable you would rather not leak. The cost is one `fork()` per execution, which is irrelevant once per script and measurable inside a loop that runs thousands of times. The trap is using `( ... )` by reflex and then wondering why the group's results disappeared. If a block both changes directory *and* needs to hand back a computed value, the subshell will silently drop the value; restructure so the value is produced some other way, or use a brace group and restore the directory yourself. ## The wider family Parentheses are the *explicit* subshell, but they are not the only fork in everyday bash. Each stage of a pipeline runs in one, a background command started with `&` runs in one, and a command substitution runs one to produce its output. Recognising `( ... )` as the visible member of that family is what makes the invisible ones predictable.
- If both forms return the last command's exit status, is there any status difference at all?No difference in the value. `( exit 3 )` and `{ (exit 3); }` both leave `$?` at 3, and a failing last command sets the group's status either way. The difference is what surrounds the status: a `set -e` or a trap installed inside `( ... )` applies only to that child, whereas inside braces it changes the running shell.
- Does a subshell created by `( ... )` re-read ~/.bashrc or start a fresh bash?No. It is a fork of the current shell, not a new bash invocation, so no startup file is read and no re-parsing happens. It inherits everything the parent had in memory, including functions and unexported variables — which is why calling a locally defined function inside `( ... )` works, while the same call inside `bash -c` would not.
- When would you deliberately avoid `( ... )` even though the isolation looks useful?When the block has to produce something the script needs afterwards — a computed variable, an array, a flag — because the subshell will discard it silently. Also inside a hot loop, where one extra `fork()` per iteration is real cost. In both cases use a brace group and manage the side effect explicitly.
saying these in an interview costs you the question
- Says braces also run the group in a separate process
- Treats the choice as pure style with no runtime effect
- Forgets the semicolon before } and blames a bash bug
- Claims the only difference is performance
- Thinks a variable set inside braces is lost afterwards