20 Shell & Python Scripting Practice Questions & Answers
Every Shell & Python Scripting practice question from the CompTIA Linux+ Practice Test, with the correct answer and a short explanation.
Start practice test →1. A backup script's first line is #!/bin/bash and the file is run as ./backup.sh. What does that first line do?
- A.It names the interpreter the kernel loads to run the file, so the remaining lines are handed to /bin/bash.✓ Answer
- B.It prepends /bin/bash to PATH for the script, so commands are searched there before any other directory.
- C.It is only a comment to the shell, so the script is interpreted by whatever shell the caller happens to run.
- D.It marks the file as runnable on its own, so setting the execute bit with chmod is no longer required.
The #! line must be the very first line; the kernel reads it and executes the named interpreter with the script as its argument. It is not a comment to the kernel, it does not alter PATH, and it does not grant the execute permission that direct execution still requires.
Source: bash(1) manual page, Invocation and the #! interpreter directiveReport a problem with this question
2. Running ./deploy.sh prints "bash: ./deploy.sh: Permission denied" and echo $? shows 126, yet bash ./deploy.sh runs the script normally. What accounts for the difference?
- A.The working directory is not listed in PATH, so the ./ form cannot be located while the interpreter form searches PATH.
- B.The interpreter directive is missing from the file, so direct execution has no interpreter while bash supplies its own.
- C.The script is owned by root, so only a sudo invocation may execute it, although reading the file stays permitted.
- D.The file is missing its execute bit, so chmod +x deploy.sh fixes direct execution; naming the interpreter only reads it.✓ Answer
Exit status 126 means the command was found but could not be executed, which for a script means the execute permission is absent; a missing file or bad path would instead yield 127. Passing the file to bash needs only read permission, so that form succeeds.
Source: POSIX.1 Shell Command Language, Exit Status; bash(1) exit status conventions (126 and 127)Report a problem with this question
3. A script is invoked as ./sync.sh /srv/data "quarter report" --verbose. Which statement about its positional parameters is correct?
- A.$# is 4 because $0 is counted, and "$@" expands to four words led by the script name.
- B.$# is 3, and "$@" expands to three words so the quoted argument stays intact.✓ Answer
- C.$# is 3, but "$@" joins the arguments into one word separated by the first IFS character.
- D.$# is 4, and "$*" keeps each argument separate while "$@" merges them into one word.
$# counts only the arguments and never $0, so it is 3 here. Quoted "$@" expands to one word per argument, which is what preserves the embedded space in "quarter report"; "$*" is the form that joins them with the first character of IFS.
Source: bash(1) manual page, Special Parameters ($#, $@ and $*)Report a problem with this question
4. A script contains f="/var/log/app log.txt" followed by if [ -f $f ]; then, and the shell reports "[: too many arguments". What is the cause and the fix?
- A.The -f operator rejects names containing whitespace, so such paths must be tested with [ -e $f ].
- B.The space had to be escaped at assignment time, because quotes are stripped before the value is stored.
- C.The unquoted expansion splits into two words, so quote it as [ -f "$f" ] to pass one argument.✓ Answer
- D.The [ builtin accepts a single operand only, so the test has to be written as [ -f $f -a -r $f ].
Word splitting happens after parameter expansion, so [ -f $f ] reaches test with three operands and test complains. Double quotes suppress that splitting and keep the path as a single argument; the assignment itself had already stored the space correctly.
Source: bash(1) manual page, Word Splitting and the test/[ builtinReport a problem with this question
5. In a script, count="01". The test [ "$count" = "1" ] is false while [ "$count" -eq 1 ] is true. Why do the two disagree?
- A.= works only inside [[ ]] and returns false in [ ], while -eq is valid in both forms and so reports a match.
- B.= strips leading zeros before comparing, so both tests ought to succeed and the false result means bad quoting.
- C.-eq orders its operands lexically, so 01 sorts immediately before 1 and the shell reports the values as equal.
- D.= compares the operands as text while -eq evaluates them as integers, so 01 differs as a string yet equals 1 numerically.✓ Answer
test's = is a string comparison and -eq is an arithmetic comparison, so a zero-padded number such as 01 is unequal as text but equal as a value. Using = where a numeric test is meant, or -eq on non-numeric text, is a classic scripting bug.
Source: bash(1) manual page, test/[ conditional expressions: string versus arithmetic operatorsReport a problem with this question
6. A script contains: case "$1" in start) echo starting ;; stop|restart) echo stopping ;; *) echo usage; exit 2 ;; esac. What happens when it is run with the argument restart?
- A.The stop|restart branch and *) both print, because * matches too and case resumes after ;;.
- B.The stop|restart branch prints stopping, and ;; ends the case so the *) branch is never tried.✓ Answer
- C.No branch runs at all, because alternative patterns need a comma and the case is skipped.
- D.The *) branch prints usage and exits 2, because case tries the default pattern before literals.
case tests its patterns from top to bottom, runs the first command list whose pattern matches, and ;; terminates that branch so execution continues after esac. The | separates alternative patterns, and *) only runs when nothing above it matched.
Source: POSIX.1 Shell Command Language, case conditional constructReport a problem with this question
7. A startup script contains: until pg_isready -h db.internal >/dev/null 2>&1; do sleep 5; done. How does that loop behave?
- A.The body keeps sleeping while pg_isready fails and the loop exits on its first success, because until runs on nonzero status.✓ Answer
- B.The body keeps sleeping while pg_isready succeeds and the loop exits on its first failure, because until is a synonym for while.
- C.The body sleeps once before the condition is ever evaluated, because until is a post-test loop like do-while in other languages.
- D.The body is skipped entirely if pg_isready fails at once, because until needs a zero status before it will enter the loop.
until executes its body as long as the condition list returns a nonzero status and stops as soon as it returns zero, which is the opposite of while. That is what makes it the natural construct for waiting until a service becomes available.
Source: POSIX.1 Shell Command Language, until loopReport a problem with this question
8. A script runs: count=0; grep -c ERROR /var/log/app.log | while read -r n; do count=$n; done; echo "$count". It always prints 0. Why?
- A.read discards the digits under the default IFS, so count is assigned an empty value on each pass.
- B.grep -c reports its count on stderr, so the loop reads nothing and count keeps its original value.
- C.Each side of a pipeline runs in its own subshell, so the loop's assignment is lost when the loop ends.✓ Answer
- D.count has to be exported before the loop so that the parent can see the child's updated value.
Commands in a pipeline execute in subshells, and a child process cannot alter its parent's variables, so count is back to 0 once the loop ends. Feeding the loop with a redirection or process substitution, as in done < <(grep -c ...), keeps it in the current shell; export only passes values downward.
Source: bash(1) manual page, Pipelines and Command Execution Environment (subshell semantics)Report a problem with this question
9. A script sets tmp=/var/tmp/keep, defines prep() { tmp=$(mktemp); }, calls prep, and then runs rm -rf "$tmp". Which change keeps the caller's value of tmp intact?
- A.Add export tmp inside prep, so the new value only reaches the child environment.
- B.Mark tmp as readonly inside prep, so the change applies only while prep is running.
- C.Declare local tmp inside prep, so the assignment stays scoped to the function body.✓ Answer
- D.Define prep with the function keyword, so every variable inside gets its own scope.
Shell functions share the caller's variable space unless a name is declared local, so prep's assignment overwrites the script's tmp. local creates a separate instance for the duration of the call; export only affects what child processes inherit, and readonly would make the assignment fail rather than scope it.
Source: bash(1) manual page, Shell Functions and the local builtinReport a problem with this question
10. A wrapper runs if /usr/local/bin/healthcheck.sh; then echo ok; else echo bad; fi, but it always takes the ok branch even when the check ends with echo "FAIL". What should the check script do?
- A.Leave the failure path with a nonzero exit status, because the caller tests the exit code.✓ Answer
- B.Send the failure message to stderr, because the shell derives success from the stream used.
- C.Print FAIL in uppercase for the caller to grep, because if inspects a command's output.
- D.Assign status=1 just before the message, because a script returns its last assigned value.
A script's exit status is its interface to any caller: if, &&, || and $? all read that number, where zero means success. Because echo itself succeeds, a script that merely prints FAIL still returns 0, so the failure path must call exit with a nonzero value.
Source: POSIX.1 Shell Command Language, Exit Status and the exit special built-inReport a problem with this question
11. A deploy script runs: mkdir -p /srv/app/rel && cp -a build/. /srv/app/rel/ || echo "deploy failed" >&2. Which description of that chain is correct?
- A.All three commands run in written order, because && and || group without testing status.
- B.cp runs only when mkdir succeeds, and the message appears only when mkdir itself fails.
- C.cp runs only when mkdir succeeds, and the message appears if either command returns nonzero.✓ Answer
- D.cp runs whatever mkdir returns, and the message appears only when cp fails, since && just groups.
&& runs the following command only when the preceding status is zero, and || runs its right-hand side when the status of the whole preceding AND-OR list is nonzero. So a failed mkdir skips cp and still triggers the message, and a failed cp triggers it as well.
Source: POSIX.1 Shell Command Language, AND-OR lists (&& and ||)Report a problem with this question
12. A long provisioning script keeps running after a failed command and silently uses an empty value from a misspelled variable name. Which single line near the top makes it fail fast instead?
- A.set -x, which prints every command before it runs and halts the script at the first one that fails.
- B.set -euo pipefail, which aborts on a failing command, an unset variable, or a failure inside a pipeline.✓ Answer
- C.shopt -s failglob, which terminates the script as soon as any command it runs returns a nonzero status.
- D.set +e, which restores the shell's default behaviour of leaving a script as soon as a command fails.
-e exits on an unhandled nonzero status, -u treats the use of an unset variable as an error, and pipefail makes a pipeline return the status of its first failing stage instead of only the last command. set +e does the opposite of -e, set -x only traces, and failglob concerns unmatched wildcards.
Source: bash(1) manual page, set builtin options -e, -u, -x and -o pipefailReport a problem with this question
13. A script builds the wrong destination path but prints no error. Which command shows each command already expanded as the shell executes it?
- A.bash -n ./script.sh, which runs the script while listing each variable assignment it performs.
- B.bash -x ./script.sh, which echoes every command with its expansions applied just before it runs.✓ Answer
- C.bash -v ./script.sh, which shows each command only after it ran, with all expansions resolved.
- D.bash -e ./script.sh, which stops at the first failure and dumps the values of all shell variables.
The -x option, equivalent to set -x inside the script, writes a trace of each simple command after expansion and prefixed by PS4, which reveals what a variable actually held. -n only parses without executing, -v echoes input lines before expansion, and -e changes error handling rather than tracing.
Source: bash(1) manual page, set -x execution tracing and shell invocation optionsReport a problem with this question
14. A script creates a temporary directory with workdir=$(mktemp -d) and exits from several places, leaving stale directories behind when it fails. What is the standard way to clean up?
- A.Register trap 'rm -rf "$workdir"' EXIT right after the directory is created, so any exit path removes it.✓ Answer
- B.Register trap 'rm -rf "$workdir"' ERR right after the directory is created, so normal endings are covered too.
- C.Put rm -rf "$workdir" on the script's last line, which every exit path reaches including early failures.
- D.Create it with mktemp -d -u instead, so the kernel deletes the directory when the script's process ends.
A trap on EXIT runs its handler whenever the shell exits, whether it fell off the end, called exit, or aborted under set -e, so the cleanup covers every path. An ERR trap fires only on failures, a final line is skipped by early exits, and -u merely makes mktemp print an unused name without creating anything.
Source: bash(1) manual page, trap builtin and the EXIT pseudo-signal; mktemp(1) manual pageReport a problem with this question
15. A provisioning script runs nightly on every host. It appends a line to /etc/hosts with >> and creates an account with useradd svc. Which description of the defect is best?
- A.It is not readable: both commands behave correctly on every run, but a reader cannot tell which change matters most.
- B.It is not atomic: the two steps are separate, so a run interrupted between them leaves the host line without its account.
- C.It is not portable: the >> redirection sends the host line to a file whose path differs across distribution families.
- D.It is not idempotent: each run adds a duplicate host line and useradd then fails, so the final state depends on the run count.✓ Answer
An idempotent script leaves the system in the same state no matter how many times it runs, which is what allows a scheduled job to be repeated safely. Appending unconditionally and calling useradd without a guard both break that, so the script must test whether the line and the account already exist before changing anything.
Source: CompTIA Linux+ exam objectives, automation and scripting domain (idempotent automated tasks); useradd(8) manual pageReport a problem with this question
16. A reporting script needs a third-party Python library on a server whose python3 comes from the distribution's package manager. What is the appropriate way to install it?
- A.Run sudo pip install for the library so every user shares one copy the package manager tracks.
- B.Point PYTHONPATH at a directory of unpacked source trees so the system interpreter finds it.
- C.Create a virtual environment with python3 -m venv, activate it, and pip install the library there.✓ Answer
- D.Use pip install --user as the service account so the distribution's package manager records it.
A virtual environment gives the project its own interpreter prefix and site-packages, so pip installations cannot collide with or overwrite files the distribution's package manager owns, and a requirements list reproduces the same set elsewhere. Installing with sudo pip into a managed interpreter risks breaking system tools, and --user or PYTHONPATH tricks leave the dependency untracked.
Source: Python Standard Library documentation, venv module (creation of virtual environments)Report a problem with this question
17. A Python script loads several thousand account records and must then look up single records by username over and over. Which structure suits those lookups best?
- A.A list of tuples in file order, because indexing a list by position reaches any record fastest.
- B.A dictionary keyed by username, because a key maps straight to its record without scanning.✓ Answer
- C.A set of the username strings, because a set holds each user together with that user's fields.
- D.A single string with the whole file, because slicing returns a named record without parsing.
A dict is a hash-based mapping, so retrieving a value by key does not depend on how many records are stored, whereas finding a record in a list means comparing entries one after another. A set stores only the keys themselves and cannot carry the remaining fields of each record.
Source: Python Standard Library documentation, built-in types: mapping type dictReport a problem with this question
18. A Python script reads a JSON configuration file and currently ends with a traceback when the file is missing or malformed. It must instead log the problem and continue with defaults. What should be added?
- A.A check with os.path.exists before the read, which also covers a file that exists but holds invalid JSON.
- B.A call to sys.exit(0) in an else clause, so an absent file counts as a clean run and the defaults load after.
- C.A bare raise at the end of the reader, which turns the traceback into a warning and lets the script go on.
- D.A try/except around the read that catches OSError and json.JSONDecodeError, logs it, then uses defaults.✓ Answer
Catching the specific exceptions lets the script decide what to do instead of terminating, and both failure modes must be handled: OSError for a missing or unreadable file and json.JSONDecodeError for invalid content. An existence check is a race and says nothing about the contents, while a bare raise re-raises the error rather than downgrading it.
Source: Python Language Reference, the try statement; Python Standard Library documentation, json module exceptionsReport a problem with this question
19. A Python maintenance script must run systemctl is-active nginx and branch on the result. Which call is the appropriate way to do that?
- A.eval on the command string, since eval hands the text to the shell and yields its exit status.
- B.os.system with the command in one string, then read its return value as the captured output.
- C.subprocess.run with shell=True and the unit name interpolated, so the shell quotes the text safely.
- D.subprocess.run with the command as a list and capture_output=True, then branch on returncode.✓ Answer
subprocess.run is the documented interface for running an external command: passing an argument list avoids a shell entirely, and the returned CompletedProcess carries returncode plus captured stdout and stderr to branch on. os.system returns a wait status rather than output, shell=True with interpolated text invites command injection, and eval evaluates Python expressions, not shell commands.
Source: Python Standard Library documentation, subprocess module (subprocess.run and security considerations)Report a problem with this question
20. An administration script must join and test filesystem paths, read a configuration file written in JSON, stamp each record with the current date, and write leveled messages to a log file. Which standard-library modules match those four jobs, in that order?
- A.pathlib, json, datetime, logging✓ Answer
- B.os.path, pickle, calendar, print
- C.glob, configparser, datetime, warnings
- D.shutil, csv, time, syslog
pathlib models filesystem paths as objects that support joining and existence tests, json parses and serialises JSON text, datetime supplies dates and timestamps, and logging emits messages at severity levels to configurable handlers. The other lists mix in modules for copying files, tabular text, object serialisation or shell globbing, none of which perform those four jobs.
Source: Python Standard Library documentation, pathlib, json, datetime and logging modulesReport a problem with this question
Practice questions written to the published CompTIA Linux+ exam objectives and to standard Linux administration practice. CompTIA and Linux+ are marks of CompTIA, and Linux is a registered trademark of Linus Torvalds; this site is not affiliated with or endorsed by CompTIA. The real exam mixes multiple-choice items with performance-based questions that ask you to carry out a task in a simulated environment — those cannot be reproduced in a four-option format, so this bank covers the knowledge half and you should practise on a real system alongside it. CompTIA revises and version-numbers the objectives periodically: confirm the current objectives, exam code and requirements with CompTIA before testing. About the CompTIA Linux+ certification →