Variables & Quoting
Variable Assignment
greeting = "Hello"
number = 42
puts greeting
puts number greeting="Hello"
number=42
echo $greeting
echo $number Bash requires no spaces around the
= sign. A space on either side causes a syntax error. Variable names are case-sensitive. Unlike Ruby, Bash has no separate integer or string types β everything is a string unless you use declare -i.Curly Brace Expansion
language = "Ruby"
puts "I love #{language}!"
puts "#{language}ist" language="Ruby"
echo "I love ${language}!"
echo "${language}ist" In Bash, curly braces around a variable name (
${variable}) are optional for simple access but required when the variable name is immediately followed by letters, digits, or underscores that should not be part of the name. Ruby's #{} syntax is analogous but always required for interpolation.Single vs Double Quotes
name = "World"
puts 'Hello, #{name}' # No interpolation β literal #{}
puts "Hello, #{name}" # Interpolation name="World"
echo 'Hello, $name' # No interpolation β literal $name
echo "Hello, $name" # Interpolation Single quotes in Bash prevent all interpretation β no variable expansion, no escape sequences. Double quotes allow variable and command substitution but prevent word splitting and glob expansion. Ruby's single-quote strings prevent interpolation similarly, but still allow
\\ and \' escapes.Default Value
port = nil
port ||= 8080
puts port echo "${port:-8080}"
# port is unset, so prints: 8080
port=3000
echo "${port:-8080}"
# port is set, so prints: 3000 The
${var:-default} expansion returns the default if the variable is unset or empty, without modifying it. This is Bash's idiomatic equivalent of Ruby's ||= for providing fallbacks. The :- variant treats both unset and empty string as "missing"; - (without colon) only triggers on unset.Assign If Unset
count ||= 0
count += 1
puts count : ${count:=0}
(( count += 1 ))
echo $count The
${var:=value} expansion assigns the value to the variable if it is unset or empty, then expands to that value. The leading : (the null command) discards the expansion result while still triggering the assignment. This is Bash's equivalent of Ruby's ||= when you want the assignment to persist.Alternate Value
debug = true
prefix = debug ? "[DEBUG] " : ""
puts "#{prefix}Server started" debug=true
echo "${debug:+[DEBUG] }Server started" The
${var:+alt} expansion returns the alternate value if the variable is set and non-empty, otherwise returns nothing. It is the inverse of ${var:-default}. Useful for optionally inserting a prefix or flag based on whether a variable is set.Readonly Variables
PI = 3.14159
puts PI
# PI = 3.0 # => NameError: already initialized constant declare -r PI=3.14159
echo $PI
# PI=3.0 # would produce: bash: PI: readonly variable The
declare -r flag marks a variable as read-only. Any attempt to reassign it causes a runtime error. In Ruby, constants (capitalized names) warn on reassignment but still allow it. Bash is stricter β reassignment is a hard error at runtime.Integer Variables
count = 0
begin
count += "5" # TypeError β cannot coerce String to Integer
rescue TypeError => error
puts error.message
end
count += 5
puts count declare -i count=0
count+=5 # Arithmetic context β adds 5
count+="hello" # Non-numeric string treated as 0
echo $count The
declare -i flag forces a variable into integer context. Assignments to it are evaluated as arithmetic expressions β string values that cannot be parsed as integers are treated as zero. This is very different from Ruby, where you cannot accidentally mix types in arithmetic.Automatic Case Transformation
tag = "HELLO"
puts tag.downcase # hello
puts tag.upcase # HELLO declare -l lowered="HELLO WORLD"
declare -u uppered="hello world"
echo $lowered # hello world
echo $uppered # HELLO WORLD The
declare -l flag auto-lowercases every value assigned to the variable; declare -u auto-uppercases. This means the transformation is applied at assignment time, not at read time. Ruby applies transforms on demand with .downcase and .upcase without changing the original string.Environment Variables
home = ENV["HOME"]
puts "Home: #{home}"
puts "Shell: #{ENV["SHELL"]}" echo "Home: $HOME"
echo "Shell: $SHELL" In Bash, environment variables inherited from the process are immediately accessible as plain variables. Ruby accesses them through the
ENV hash. Note that Bash variable names are conventionally uppercase for environment variables and lowercase for local script variables, though Bash itself enforces no such distinction.String Operations
String Length
message = "Hello, World!"
puts message.length # 13 message="Hello, World!"
echo ${#message} # 13 The
${#variable} expansion returns the number of characters in the string. In Ruby, .length and .size are equivalent methods. For arrays, the same Bash syntax (${#array[@]}) returns the element count.Substring Extraction
text = "Hello, World!"
puts text[7, 5] # World
puts text[7..] # World! text="Hello, World!"
echo ${text:7:5} # World
echo ${text:7} # World! The
${var:offset:length} expansion extracts a substring starting at offset (0-indexed). Omitting :length returns everything from the offset to the end. Negative offsets count from the end (but require a space: ${var: -3}). Ruby's slice syntax [start, length] maps directly to this.Uppercase & Lowercase
greeting = "Hello, World!"
puts greeting.upcase # HELLO, WORLD!
puts greeting.downcase # hello, world!
puts greeting.capitalize # Hello, world! greeting="Hello, World!"
echo ${greeting^^} # HELLO, WORLD!
echo ${greeting,,} # hello, world!
echo ${greeting^} # Hello, World! (first char only)
echo ${greeting,} # hELLO, WORLD! (first char lowercase) Bash 4+ supports case-modification parameter expansions:
^^ uppercases all characters, ,, lowercases all, ^ uppercases only the first character, and , lowercases only the first. Ruby provides .upcase, .downcase, and .capitalize as equivalent methods.Replace First Occurrence
text = "the cat sat on the mat"
puts text.sub("at", "og") # the cog sat on the mat text="the cat sat on the mat"
echo ${text/at/og} # the cog sat on the mat The
${var/pattern/replacement} expansion replaces the first match of the glob pattern. Ruby's String#sub is equivalent but uses a Regexp by default. Note that Bash patterns use glob syntax (*, ?, [...]), not regular expressions.Replace All Occurrences
text = "the cat sat on the mat"
puts text.gsub("at", "og") # the cog sog on the mog text="the cat sat on the mat"
echo ${text//at/og} # the cog sog on the mog The
${var//pattern/replacement} expansion (double slash) replaces all occurrences. The single-slash version only replaces the first. Ruby's String#gsub is the equivalent. Unlike gsub, the Bash pattern is a glob, not a regexp.Strip Prefix
path = "/usr/local/bin/ruby"
# Remove leading slash and up to next slash:
puts path.delete_prefix("/usr/") # local/bin/ruby
puts path.sub(%r{^/[^/]*/}, "") # local/bin/ruby path="/usr/local/bin/ruby"
echo ${path#/usr/} # local/bin/ruby (shortest match)
echo ${path##/*/} # local/bin/ruby (longest prefix up to /) The
${var#pattern} expansion removes the shortest prefix matching the glob; ${var##pattern} removes the longest matching prefix. This is frequently used to strip directory components from paths. Ruby uses delete_prefix, sub, or regexp operations for the same task.Strip Suffix
filename = "archive.tar.gz"
puts filename.delete_suffix(".gz") # archive.tar
puts filename.sub(/.[^.]*$/, "") # archive.tar (last ext)
puts filename.sub(/..*$/, "") # archive (all exts) filename="archive.tar.gz"
echo ${filename%.gz} # archive.tar (shortest suffix)
echo ${filename%.*} # archive.tar (last extension)
echo ${filename%%.*} # archive (all extensions) The
${var%pattern} expansion removes the shortest matching suffix; ${var%%pattern} removes the longest. Together with the prefix operators, these four expansions cover most path and filename manipulation tasks without calling external tools.Test String Contains
sentence = "The quick brown fox"
if sentence.include?("quick")
puts "Found it"
end sentence="The quick brown fox"
if [[ $sentence == *"quick"* ]]; then
echo "Found it"
fi In Bash, the
== operator inside [[ ]] performs glob matching when the right side is unquoted. Wrapping the pattern in *...* checks for a substring anywhere in the string. Quoting the entire right side would do a literal string comparison instead.String Concatenation
first = "Hello"
second = "World"
puts first + ", " + second + "!"
puts "#{first}, #{second}!" first="Hello"
second="World"
echo "$first, $second!"
combined="$first, $second!"
echo $combined Bash has no string concatenation operator. Strings are simply placed adjacent to each other inside double quotes. There is no
+ operator for strings β using it would attempt arithmetic and likely produce an error or zero. Ruby's + and #{} interpolation are both cleaner for this.Build a Repeated String
puts "-" * 20
puts "ha" * 3 separator=""
for (( i=0; i<20; i++ )); do
separator+="-"
done
echo $separator
repeated=""
for (( i=0; i<3; i++ )); do
repeated+="ha"
done
echo $repeated Bash has no string repetition operator. The idiomatic approach is a loop with
+= concatenation. Ruby's "str" * n is far more concise. The printf builtin can also build repeated strings using format widths but in a roundabout way.Split a String (Manual)
csv_line = "alice,bob,charlie"
parts = csv_line.split(",")
puts parts.inspect
puts parts[1] csv_line="alice,bob,charlie"
IFS="," read -ra parts <<< "$csv_line"
echo "${parts[@]}"
echo "${parts[1]}" Bash splits strings using the Internal Field Separator (
IFS). Setting IFS="," before read -ra splits the here-string into array elements. The -r flag disables backslash escaping; -a reads into an array. Ruby's String#split is much simpler and more powerful.Trim Whitespace (Manual)
text = " hello world "
puts text.strip
puts text.lstrip
puts text.rstrip text=" hello world "
# Strip leading whitespace
trimmed_left="${text#"${text%%[! ]*}"}"
echo "'$trimmed_left'"
# Strip trailing whitespace
trimmed_right="${text%"${text##*[! ]}"}"
echo "'$trimmed_right'" Bash has no built-in trim function. The idiomatic approach nests parameter expansions:
${var%%[! ]*} extracts the leading spaces, then ${var#...} strips that prefix. This works but is deliberately obscure β it highlights why Ruby's .strip is a real quality-of-life advantage.Arithmetic
Arithmetic Expansion
puts 10 + 3 # 13
puts 10 - 3 # 7
puts 10 * 3 # 30
puts 10 / 3 # 3 (integer division)
puts 10 % 3 # 1
puts 2 ** 8 # 256 echo $(( 10 + 3 ))
echo $(( 10 - 3 ))
echo $(( 10 * 3 ))
echo $(( 10 / 3 )) # Integer division: 3
echo $(( 10 % 3 ))
echo $(( 2 ** 8 )) The
$(( )) construct is arithmetic expansion β it evaluates integer arithmetic and expands to the result as a string. This is entirely different from $() command substitution (which requires fork()). Bash only supports integer arithmetic natively; use bc or awk for floating point (external tools not available in browser).Compound Arithmetic Tests
x = 10
x += 5
x -= 2
puts x # 13 x=10
(( x += 5 ))
(( x -= 2 ))
echo $x # 13 The
(( )) construct (without the leading $) evaluates arithmetic and is used as a command β it returns exit code 0 if the result is non-zero, 1 if zero. This makes it usable directly in if and while conditions. All C-style compound operators work: +=, -=, *=, /=, %=, **=.Increment & Decrement
counter = 0
counter += 1
puts counter # 1
counter += 1
puts counter # 2 counter=0
(( counter++ ))
echo $counter # 1
(( ++counter ))
echo $counter # 2
(( counter-- ))
echo $counter # 1 Bash supports C-style post-increment (
counter++) and pre-increment (++counter) inside (( )). The difference: post-increment returns the value before incrementing; pre-increment returns the value after. Ruby has no ++ or -- operators β use += 1 instead.Arithmetic Comparisons
score = 85
puts score >= 90 ? "A" : (score >= 80 ? "B" : "C") score=85
if (( score >= 90 )); then
echo "A"
elif (( score >= 80 )); then
echo "B"
else
echo "C"
fi Inside
(( )), comparison operators use C syntax: ==, !=, <, >, <=, >=. This is much cleaner than the legacy -eq, -lt, -gt etc. flags used with [ ] or [[ ]] for numeric comparisons. The (( )) style is unambiguous and preferred.Ternary in Arithmetic
x = 7
result = x.even? ? "even" : "odd"
puts result x=7
echo $(( x % 2 == 0 ? 0 : 1 )) # 1 (x is odd)
(( x % 2 == 0 )) && echo "even" || echo "odd" Arithmetic expansion supports the C ternary operator
cond ? yes : no, but it only works for integer results, not strings. The && ... || ... idiom chains shell commands β it works like a ternary for string output but can misbehave if the "then" command fails. Use an explicit if for clarity.Floating-Point Formatting
pi = 3.14159265
puts pi.round(2) # 3.14
printf("%.4f
", pi) # 3.1416 pi=3.14159265
printf "%.2f
" $pi # 3.14
printf "%.4f
" $pi # 3.1416 Bash's arithmetic is integer-only β
$(( 1/3 )) produces 0, not 0.333. The printf builtin can format floating-point literals (strings that look like floats) using %f. For actual float arithmetic, you would normally use bc or awk (external tools not available in the browser runtime).The let Builtin
product = 6 * 7
puts product # 42 let "product = 6 * 7"
echo $product # 42
let "x = 2 ** 10"
echo $x # 1024 The
let builtin evaluates arithmetic expressions and assigns results to variables. It is older than (( )) and less commonly used in modern Bash scripts. Both forms are equivalent β (( product = 6 * 7 )) is the contemporary idiom. let is more readable for simple single-expression assignments.Indexed Arrays
Create & Access
fruits = ["apple", "banana", "cherry"]
puts fruits[0] # apple
puts fruits[1] # banana
puts fruits[-1] # cherry fruits=("apple" "banana" "cherry")
echo ${fruits[0]} # apple
echo ${fruits[1]} # banana
echo ${fruits[-1]} # cherry (bash 4.1+) Bash arrays use parentheses for initialization and
${array[index]} for access (0-indexed). Without the ${}, just writing $array expands to only the first element. Negative indices (Bash 4.1+) count from the end, matching Ruby's behavior.Array Length
fruits = ["apple", "banana", "cherry"]
puts fruits.length # 3 fruits=("apple" "banana" "cherry")
echo ${#fruits[@]} # 3 The
${#array[@]} expansion returns the number of elements. The @ means "all elements" β using * instead also works but behaves differently inside double quotes (it joins elements with the first character of IFS rather than producing separate words).All Elements
fruits = ["apple", "banana", "cherry"]
puts fruits.join(" ")
puts fruits.inspect fruits=("apple" "banana" "cherry")
echo "${fruits[@]}" # apple banana cherry
printf "%s
" "${fruits[@]}" # one per line Inside double quotes,
"${array[@]}" expands to one separate word per element, preserving spaces within elements. "${array[*]}" joins all elements into a single word separated by the first character of IFS. Always prefer [@] inside double quotes when iterating.Append Elements
fruits = ["apple", "banana"]
fruits << "cherry"
fruits.push("date")
puts fruits.inspect fruits=("apple" "banana")
fruits+=("cherry")
fruits+=("date" "elderberry")
echo "${fruits[@]}" The
+= operator appends elements to an array when the right-hand side is a parenthesized list. This is analogous to Ruby's << or push. You can append a single element or multiple at once. You can also assign directly: fruits[5]="fig" β Bash arrays are sparse.Array Slice
fruits = ["apple", "banana", "cherry", "date"]
puts fruits[1, 2].inspect # ["banana", "cherry"] fruits=("apple" "banana" "cherry" "date")
echo "${fruits[@]:1:2}" # banana cherry The
${array[@]:offset:length} expansion slices an array, returning elements starting at offset with the given length. Without :length, it returns all elements from the offset to the end. This mirrors Ruby's array[start, length] slice syntax.Iterate Over Array
fruits = ["apple", "banana", "cherry"]
fruits.each do |fruit|
puts fruit
end fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
echo "$fruit"
done Always quote
"${fruits[@]}" in the for loop. Without quotes, elements containing spaces would be split into multiple words. The double-quoted [@] expansion ensures each array element is treated as a single word, matching Ruby's block-based iteration.Array Indices
fruits = ["apple", "banana", "cherry"]
fruits.each_with_index do |fruit, index|
puts "#{index}: #{fruit}"
end fruits=("apple" "banana" "cherry")
for index in "${!fruits[@]}"; do
echo "$index: ${fruits[$index]}"
done The
${!array[@]} expansion returns the list of indices (keys) of the array. For a standard indexed array this is 0 1 2 ..., but since Bash arrays are sparse, gaps are reflected accurately. For associative arrays, this returns the string keys β the same syntax works for both types.Delete an Element
fruits = ["apple", "banana", "cherry"]
fruits.delete_at(1)
puts fruits.inspect # ["apple", "cherry"] fruits=("apple" "banana" "cherry")
unset "fruits[1]"
echo "${fruits[@]}" # apple cherry
echo "${!fruits[@]}" # 0 2 (gap at index 1!) Unsetting an element removes it but leaves a gap in the index sequence. Bash arrays are sparse β after
unset fruits[1], the array has indices 0 and 2 with no element at 1. Ruby's delete_at shifts subsequent elements down. To compact a Bash array after deletion, reassign it to itself: fruits=("${fruits[@]}").Brace Expansion Range
(1..5).each { |i| puts i }
(0..10).step(2) { |i| puts i } for i in {1..5}; do
echo $i
done
for i in {0..10..2}; do
echo $i
done Brace expansion (
{start..end} and {start..end..step}) generates a sequence of words at parse time β before variables are expanded. This means you cannot use a variable inside brace expansion directly: {1..$n} does not work. Use a C-style for (( i=0; i loop instead. Associative Arrays
Create & Access
person = { name: "Alice", age: 30, language: "Ruby" }
puts person[:name] # Alice
puts person[:age] # 30 declare -A person
person[name]="Alice"
person[age]=30
person[language]="Ruby"
echo ${person[name]} # Alice
echo ${person[age]} # 30 Associative arrays require
declare -A before use β without it, Bash treats the variable as an ordinary indexed array and the string key is ignored (coerced to 0). Unlike Ruby hashes, Bash associative arrays only support string keys and string values, and their iteration order is undefined.Initialize with Values
colors = { red: "#FF0000", green: "#00FF00", blue: "#0000FF" }
puts colors[:red] declare -A colors=([red]="#FF0000" [green]="#00FF00" [blue]="#0000FF")
echo ${colors[red]} Associative arrays can be initialized inline using the
([key]=value ...) syntax inside parentheses, combined with declare -A. The key must be in brackets. This mirrors Ruby's hash literal syntax but is less readable due to the required brackets and parentheses.All Keys & Values
scores = { alice: 95, bob: 87, carol: 92 }
puts scores.keys.inspect
puts scores.values.inspect declare -A scores=([alice]=95 [bob]=87 [carol]=92)
echo "${!scores[@]}" # alice bob carol (order varies)
echo "${scores[@]}" # 95 87 92 (order varies) The
${!assoc[@]} expansion returns all keys; ${assoc[@]} returns all values. Iteration order is undefined (hash-based internally) β unlike Ruby where hashes preserve insertion order since Ruby 1.9. Always account for arbitrary key order when writing scripts.Check If Key Exists
colors = { red: "#FF0000", green: "#00FF00" }
puts colors.key?(:red) # true
puts colors.key?(:purple) # false declare -A colors=([red]="#FF0000" [green]="#00FF00")
if [[ -v colors[red] ]]; then
echo "red exists"
fi
if [[ ! -v colors[purple] ]]; then
echo "purple does not exist"
fi The
[[ -v array[key] ]] test (Bash 4.2+) checks whether a key exists in an associative array without expanding the value. This is important because an existing key with an empty string value should still be "present". Do not use [[ -n ${assoc[key]} ]] β that conflates an empty-string value with a missing key.Iterate Key-Value Pairs
inventory = { apples: 5, bananas: 3, cherries: 12 }
inventory.each do |item, count|
puts "#{item}: #{count}"
end declare -A inventory=([apples]=5 [bananas]=3 [cherries]=12)
for item in "${!inventory[@]}"; do
echo "$item: ${inventory[$item]}"
done Iterate over keys with
"${!assoc[@]}" and then look up each value inside the loop. The double-quoted [@] form is essential to handle keys that contain spaces. Iteration order is undefined β sort with printf "%s\n" "${!inventory[@]}" | sort for deterministic output (pipe not available in browser).Delete a Key
settings = { debug: true, verbose: false, timeout: 30 }
settings.delete(:verbose)
puts settings.keys.inspect declare -A settings=([debug]=true [verbose]=false [timeout]=30)
unset "settings[verbose]"
echo "${!settings[@]}" The
unset builtin removes a key from an associative array. Quote the expression to prevent glob expansion of the key. After deletion, the key no longer appears in ${!settings[@]} and [[ -v settings[verbose] ]] returns false.Number of Keys
inventory = { apples: 5, bananas: 3, cherries: 12 }
puts inventory.size # 3 declare -A inventory=([apples]=5 [bananas]=3 [cherries]=12)
echo ${#inventory[@]} # 3 The same
${#array[@]} syntax that counts elements in an indexed array also counts keys in an associative array. This is consistent across both array types in Bash.Control Flow
if / elif / else
temperature = 72
if temperature > 85
puts "Hot"
elsif temperature > 65
puts "Comfortable"
else
puts "Cold"
end temperature=72
if (( temperature > 85 )); then
echo "Hot"
elif (( temperature > 65 )); then
echo "Comfortable"
else
echo "Cold"
fi Bash uses
then, elif, and fi (if backwards) to delimit conditional blocks. The then must be on a new line or preceded by a semicolon. Using (( )) for numeric tests is the modern idiom β it supports all C-style comparison operators without the awkward -gt/-lt flags.String Comparisons
language = "ruby"
puts language == "ruby" # true
puts language != "python" # true
puts language.empty? # false
puts language.length > 0 # true language="ruby"
[[ $language == "ruby" ]] && echo "match"
[[ $language != "python" ]] && echo "not python"
[[ -z $language ]] && echo "empty" || echo "not empty"
[[ -n $language ]] && echo "has content" The
[[ ]] construct (double brackets) is the modern conditional expression. It supports == and != for string comparison, -z for "zero length", and -n for "non-zero length". The [[ ]] is safer than the older [ ] because it handles empty variables without quoting issues.Numeric Comparisons
x = 5
puts x == 5 # true
puts x < 10 # true
puts x >= 5 # true x=5
[[ $x -eq 5 ]] && echo "equals 5" # Legacy flags
[[ $x -lt 10 ]] && echo "less than 10"
(( x == 5 )) && echo "equals 5" # Modern (( ))
(( x < 10 )) && echo "less than 10" Numeric comparisons in Bash can use either legacy flags (
-eq, -ne, -lt, -le, -gt, -ge) inside [[ ]], or C-style operators (==, <, etc.) inside (( )). The (( )) style is more natural for arithmetic comparisons.Logical Operators
age = 25
is_member = true
if age >= 18 && is_member
puts "Access granted"
end age=25
is_member=true
if (( age >= 18 )) && [[ $is_member == "true" ]]; then
echo "Access granted"
fi Bash uses
&& and || for logical AND and OR both between separate commands and inside [[ ]]. You can combine tests from different constructs ((( )) and [[ ]]) with &&. Avoid the older -a and -o flags inside [ ] β they are deprecated.case / esac
day = "Monday"
case day
when "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
puts "Weekday"
when "Saturday", "Sunday"
puts "Weekend"
else
puts "Unknown"
end day="Monday"
case $day in
Monday|Tuesday|Wednesday|Thursday|Friday)
echo "Weekday"
;;
Saturday|Sunday)
echo "Weekend"
;;
*)
echo "Unknown"
;;
esac Bash's
case/esac is the inverse spelling of case (like fi for if). Patterns use | for alternatives and ;; to end each branch. Patterns are glob expressions, not regexes. The * default case is the equivalent of Ruby's else.case with Glob Patterns
filename = "report.pdf"
case filename
when /.txt$/ then puts "Text file"
when /.pdf$/ then puts "PDF document"
when /.(jpg|png)$/ then puts "Image"
else puts "Unknown type"
end filename="report.pdf"
case $filename in
*.txt) echo "Text file" ;;
*.pdf) echo "PDF document" ;;
*.jpg|*.png) echo "Image" ;;
*) echo "Unknown type" ;;
esac The
case statement is especially powerful for matching file extensions and other glob patterns. The *. prefix matches any string followed by a dot and extension. This replaces the need for many if/elif chains. Ruby's equivalent uses case/when with regexps, which are more powerful but heavier.Short-Circuit Evaluation
dir = "/tmp"
File.exist?(dir) && puts("#{dir} exists")
# Or:
puts "Dir exists" if File.exist?(dir) dir="/tmp"
# && runs right side only if left side succeeds (exit code 0)
[[ -d $dir ]] && echo "$dir exists"
# || runs right side only if left side fails
[[ -d /nonexistent ]] || echo "Does not exist" Bash's
&& and || short-circuit at the command level based on exit codes, not boolean values. Exit code 0 is "success" (truthy); any non-zero is "failure" (falsy). This is the opposite of most languages where 0 is falsy. The pattern command && on-success || on-failure mimics a ternary but can misbehave if the success branch fails.Loops
for-in Loop
["red", "green", "blue"].each do |color|
puts color
end for color in red green blue; do
echo $color
done The
for item in list; do ... done loop iterates over whitespace-separated words. The list is subject to word splitting and glob expansion β be careful with spaces. Always quote array expansions: for item in "${array[@]}". Ruby's .each block is semantically identical.C-Style for Loop
5.times { |i| puts i }
0.upto(4) { |i| puts i } for (( i=0; i<5; i++ )); do
echo $i
done The C-style
for (( init; condition; increment )) loop uses arithmetic context throughout β no $ needed inside. This is the idiomatic Bash replacement for Ruby's .times or .upto when you need an index. The loop variable is available after the loop ends.while Loop
count = 0
while count < 5
puts count
count += 1
end count=0
while (( count < 5 )); do
echo $count
(( count++ ))
done The
while condition; do ... done loop runs as long as the condition command returns exit code 0. Using (( )) as the condition gives C-style arithmetic tests. Unlike Ruby's while, the Bash loop body must end with a separator (; or newline) before done.until Loop
count = 0
until count >= 5
puts count
count += 1
end count=0
until (( count >= 5 )); do
echo $count
(( count++ ))
done The
until loop runs as long as the condition is false (exit code non-zero). It is the logical inverse of while. Ruby has both while and until with the same inversion semantics. In practice, until is less common in both languages β a negated while is often clearer.break & continue
(1..10).each do |i|
next if i.even?
break if i > 7
puts i
end for (( i=1; i<=10; i++ )); do
(( i % 2 == 0 )) && continue
(( i > 7 )) && break
echo $i
done Bash's
break and continue work exactly like Ruby's break and next for loops. Both accept an optional numeric argument to break or continue an outer loop: break 2 exits the two innermost loops. This is more explicit than Ruby's break in nested loops.Brace Expansion in Loops
('a'..'e').each { |letter| puts letter }
(1..10).step(3) { |n| puts n } for letter in {a..e}; do
echo $letter
done
for n in {1..10..3}; do
echo $n
done Brace expansion generates a sequence of words at parse time, before any variables are resolved. This means
{1..n} where n is a variable does not work β use a C-style for (( i=1; i<=n; i++ )) loop instead. Ruby's Range with .step is more flexible.Read Lines of Input
lines = ["one", "two", "three"]
lines.each do |line|
puts "Line: #{line}"
end # Reading from a pipe: not available in browser (requires fork)
# Standard pattern for reference:
# while IFS= read -r line; do
# echo "Line: $line"
# done < file.txt The canonical Bash pattern for reading file content line by line uses
while IFS= read -r line; do ... done < file. The IFS= prevents leading/trailing whitespace stripping; -r disables backslash processing. This pattern is not runnable in the browser (no real filesystem or pipes), but is fundamental to real-world shell scripting.Functions
Define & Call a Function
def greet(name)
puts "Hello, #{name}!"
end
greet("Alice")
greet("Bob") greet() {
echo "Hello, $1!"
}
greet "Alice"
greet "Bob" Bash functions are defined with
name() { ... } or function name { ... }. They are called like commands β no parentheses, arguments separated by spaces. Parameters are positional: $1 is the first argument, $2 is the second, etc. Unlike Ruby, there is no parameter naming in the function signature.Parameters & Argument Count
def describe(*args)
puts "Got #{args.length} arguments: #{args.join(", ")}"
end
describe("one", "two", "three") describe() {
echo "Got $# arguments: $@"
}
describe "one" "two" "three" Inside a Bash function,
$# is the argument count and $@ is all arguments as separate words. $* joins all arguments into one string. Use "$@" (quoted) when passing arguments forward to preserve spacing. These special variables are reset to function scope β they do not bleed out to the calling script.Local Variables
counter = 10
def increment
counter = 0 # Local β does not affect outer
counter += 1
puts "Inside: #{counter}"
end
increment
puts "Outside: #{counter}" counter=10
increment() {
local counter=0 # Must use 'local' keyword
(( counter++ ))
echo "Inside: $counter"
}
increment
echo "Outside: $counter" In Bash, variables inside functions are global by default β without
local, assigning to a variable inside a function modifies the outer scope. This is the opposite of Ruby, where all variables are local to their scope. Always use local for function-internal variables to avoid subtle bugs.Return Values
def is_even?(number)
number.even?
end
if is_even?(4)
puts "4 is even"
end is_even() {
(( $1 % 2 == 0 )) # Returns 0 (true) if even, 1 (false) if odd
}
if is_even 4; then
echo "4 is even"
fi
if ! is_even 7; then
echo "7 is odd"
fi Bash functions do not return values β they return exit codes (0 = success/true, 1β255 = failure/false). The exit code is set by the last command executed or by an explicit
return n statement. An arithmetic expression like (( n % 2 == 0 )) exits 0 when the expression is non-zero (even), and 1 when zero (odd) β exactly backwards from arithmetic convention.Returning a String via Global Variable
def double(number)
number * 2
end
result = double(21)
puts result # 42 double() {
# Cannot use $() to capture β use a global "return" variable
result_var=$(( $1 * 2 ))
}
double 21
echo $result_var # 42 Since Bash functions only return exit codes, returning a string value requires a workaround: write to a known global variable. The convention is to use a variable like
result_var or a nameref (declare -n). The $(function_call) pattern (command substitution) also works but requires fork() which is unavailable in this browser runtime.Default Parameter Values
def greet(name = "World")
puts "Hello, #{name}!"
end
greet # Hello, World!
greet("Alice") # Hello, Alice! greet() {
local name="${1:-World}"
echo "Hello, $name!"
}
greet # Hello, World!
greet "Alice" # Hello, Alice! Bash has no default parameter syntax in the function signature. The idiomatic substitute is
local var="${n:-default}" inside the function body β this uses the positional parameter if provided, otherwise uses the default. Ruby's def foo(x = default) is cleaner and handles any expression as the default value.Recursive Function
def factorial(number)
return 1 if number <= 1
number * factorial(number - 1)
end
puts factorial(5) # 120 factorial() {
if (( $1 <= 1 )); then
echo 1
return
fi
local prev
factorial $(( $1 - 1 ))
prev=$result_var
result_var=$(( $1 * prev ))
}
factorial 5
echo $result_var # 120 Recursion works in Bash but is awkward since return values must flow through global variables rather than a clean call stack. The
local keyword correctly scopes variables to each invocation. Deep recursion is expensive in Bash β each function call is a new stack frame, and Bash scripts are much slower than Ruby for recursive algorithms.Nameref Variables (Reference Parameters)
def push_item(collection, item)
collection << item
end
items = []
push_item(items, "apple")
puts items.inspect # ["apple"] push_item() {
declare -n collection=$1 # nameref to the variable named by $1
collection+=("$2")
}
items=()
push_item items "apple"
echo "${items[@]}" # apple declare -n (Bash 4.3+) creates a nameref β a reference to another variable by name. The nameref variable acts as an alias: reading or writing it reads or writes the target variable. This is Bash's equivalent of passing an object by reference in Ruby, enabling functions to modify arrays and hashes in the caller's scope.Pattern Matching
Glob Pattern Matching
filename = "report_2026.txt"
if filename.end_with?(".txt")
puts "Text file"
end
if filename.start_with?("report")
puts "Is a report"
end filename="report_2026.txt"
if [[ $filename == *.txt ]]; then
echo "Text file"
fi
if [[ $filename == report* ]]; then
echo "Is a report"
fi Inside
[[ ]], the right-hand side of == and != is treated as a glob pattern when unquoted. * matches any sequence of characters, ? matches exactly one. Quoting the pattern forces a literal string comparison. This is the fastest way to do prefix/suffix/substring checks in Bash.Regular Expression Matching
email = "user@example.com"
if email.match?(/\A[\w.%+-]+@[\w.-]+\.[a-z]{2,}\z/i)
puts "Valid email"
end email="user@example.com"
if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Valid email"
fi The
=~ operator inside [[ ]] matches the left side against an extended regular expression (ERE). The pattern is unquoted β quoting it forces a literal string match. Bash uses POSIX ERE syntax (no \d, use [0-9] instead). Successful matches set BASH_REMATCH with the captured groups.Regex Capture Groups
version = "2.3.1"
if (match = version.match(/^(\d+)\.(\d+)\.(\d+)$/))
puts "Major: #{match[1]}"
puts "Minor: #{match[2]}"
puts "Patch: #{match[3]}"
end version="2.3.1"
if [[ $version =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "Major: ${BASH_REMATCH[1]}"
echo "Minor: ${BASH_REMATCH[2]}"
echo "Patch: ${BASH_REMATCH[3]}"
fi After a successful
=~ match, BASH_REMATCH[0] holds the full match and BASH_REMATCH[1], [2], etc. hold each capture group. This is equivalent to Ruby's MatchData object. The BASH_REMATCH array is read-only and set globally β in functions, save it before any subsequent [[ =~ ]] overwrites it.Extended Glob Patterns
filename = "config.yaml"
unless filename.match?(/\.(txt|md)$/)
puts "Not a text or markdown file"
end shopt -s extglob
filename="config.yaml"
if [[ $filename != *.@(txt|md) ]]; then
echo "Not a text or markdown file"
fi Extended glob patterns (
shopt -s extglob) add regex-like quantifiers to glob syntax: @(a|b) matches exactly one of the alternatives, *(a|b) matches zero or more, +(a|b) matches one or more, ?(a|b) matches zero or one, and !(pattern) matches anything except the pattern. These are often cleaner than regex for file-matching tasks.Pattern Matching in case
input = "yes"
case input
when /^y(es)?$/i then puts "Affirmative"
when /^no?$/i then puts "Negative"
else puts "Unknown"
end input="yes"
case $input in
[Yy]|[Yy][Ee][Ss]) echo "Affirmative" ;;
[Nn]|[Nn][Oo]) echo "Negative" ;;
*) echo "Unknown" ;;
esac case patterns support character classes ([YyNn]), alternation (pat1|pat2), and glob wildcards (*, ?). They do not support regular expressions β for regex matching, use [[ =~ ]]. The character-class approach shown here is idiomatic for case-insensitive yes/no prompts in Bash.Error Handling
Exit Codes & $?
begin
raise "Something went wrong"
rescue => error
puts "Error: #{error.message}"
puts "Handling it..."
end false # A command that always fails (exit code 1)
echo "Exit code: $?"
true # A command that always succeeds (exit code 0)
echo "Exit code: $?" Every command in Bash produces an exit code stored in
$?. Exit code 0 means success; any other value means failure. This is the fundamental error-signaling mechanism β there are no exceptions. The builtins true and false exist solely to produce exit codes 0 and 1. Ruby's exceptions are a higher-level abstraction over this concept.Automatic Exit on Error
# Ruby raises exceptions by default; you must handle them explicitly
def risky
raise "Oops"
end
# risky # Would propagate and crash unless rescued set -e # Exit immediately if any command fails
echo "Step 1"
echo "Step 2"
# false # Uncommenting this would exit the script here
echo "Step 3"
set +e # Turn off strict mode set -e (or set -o errexit) makes the script exit immediately if any command returns a non-zero exit code. This makes Bash scripts behave more like Ruby β failing early rather than silently continuing. set +e turns it off. Many production scripts combine set -euo pipefail for maximum strictness.Unset Variable Protection
# Ruby raises NameError for undefined variables
# undefined_variable # => NameError: undefined local variable set -u # Treat unset variables as errors
name="Alice"
echo "Hello, $name"
# echo "Hello, $undefined" # Would cause: unbound variable
set +u set -u (or set -o nounset) causes the script to exit with an error if any unset variable is expanded. By default, Bash silently expands unset variables to empty strings β a common source of bugs. With set -u, Bash behaves more like Ruby's strict variable-must-be-defined enforcement.Error Handling with ||
def connect(host)
raise "Connection failed to #{host}" unless host == "localhost"
"Connected to #{host}"
end
begin
result = connect("remotehost")
rescue => error
puts "Error: #{error.message}"
puts "Using fallback"
end connect() {
[[ $1 == "localhost" ]] || return 1
echo "Connected to $1"
}
connect "remotehost" || {
echo "Connection failed β using fallback"
} The
command || { fallback commands; } pattern is Bash's idiomatic error handler. The right side of || runs only when the left side fails (non-zero exit code). The braces group multiple fallback commands. This is more concise than if ! command; then ... fi for simple one-shot error handling.Cleanup with trap
at_exit { puts "Cleanup!" }
puts "Working..."
# Cleanup! is printed when the script exits cleanup() {
echo "Cleanup!"
}
trap cleanup EXIT # Run cleanup() when the script exits
echo "Working..."
# "Cleanup!" prints when the script reaches the end trap registers a command to run when a signal or special event occurs. EXIT triggers on any exit (normal or error). INT triggers on Ctrl-C. ERR triggers when any command fails (useful with set -e). This is Bash's equivalent of Ruby's at_exit and ensure blocks.Output & Formatting
echo vs printf
puts "Hello, World!" # Adds newline
print "Hello, World!" # No newline
printf "Name: %s
", "Alice" echo "Hello, World!" # Adds newline
printf "Hello, World!" # No newline
printf "Hello, World!
" # With explicit newline
printf "Name: %s
" "Alice" echo always appends a newline (suppress with -n). printf uses C-style format strings and appends no newline unless you include \n. In scripts, printf is preferred over echo because echo behavior varies across shells and operating systems for flags like -e.printf Format Strings
printf("%-10s %5d %8.2f
", "Alice", 30, 98.6)
printf("Hex: %x, Oct: %o
", 255, 255) printf "%-10s %5d %8.2f
" "Alice" 30 98.6
printf "Hex: %x, Oct: %o
" 255 255 Bash's
printf supports the same format specifiers as C: %s (string), %d (integer), %f (float), %x (hex), %o (octal). Width and precision modifiers (%10s, %-10s, %.2f) work identically to Ruby's Kernel#printf.ANSI Color Codes
puts "e[32mGreen texte[0m"
puts "e[1;34mBold bluee[0m"
puts "e[31;47mRed on whitee[0m" printf "\033[32mGreen text\033[0m\n"
printf "\033[1;34mBold blue\033[0m\n"
printf "\033[31;47mRed on white\033[0m\n" ANSI escape sequences are identical in Ruby and Bash β the escape character is
\033 (octal) in Bash or \e in Ruby. Color codes: 30β37 for foreground colors, 40β47 for backgrounds, 0 to reset, 1 for bold. In Bash, use printf rather than echo for portability, as echo -e is not POSIX.Formatted Table Output
header = "%-12s %8s %10s"
printf(header + "
", "Name", "Score", "Grade")
printf("-" * 32 + "
")
printf(header + "
", "Alice", 95, "A")
printf(header + "
", "Bob", 87, "B") header="%-12s %8s %10s
"
printf "$header" "Name" "Score" "Grade"
printf "%s
" "--------------------------------"
printf "$header" "Alice" "95" "A"
printf "$header" "Bob" "87" "B" Storing a format string in a variable and reusing it with
printf "$format" is idiomatic Bash for consistent tabular output. Width specifiers align columns: %-12s left-aligns in 12 characters; %8s right-aligns in 8. This approach avoids the need for external tools like column or awk.Special Shell Variables
puts $0 # Script name (in Ruby: $PROGRAM_NAME)
puts $$ # Process ID (in Ruby: Process.pid)
puts $?.to_i # Last exit code (in Ruby: after system()) echo "Script name: $0"
echo "Process ID: $$"
echo "Bash version: $BASH_VERSION"
echo "Hostname: $HOSTNAME"
false
echo "Last exit: $?" Bash has many automatic special variables:
$0 (script name), $$ (process ID), $? (last exit code), $! (last background PID), $BASH_VERSION, $HOSTNAME, $RANDOM (random integer), $SECONDS (seconds since shell start). Ruby mirrors some of these via $$, $0, and $?.Here-Documents
text = <<~HEREDOC
Dear #{name},
Your order ##{order_id} has shipped.
Thanks!
HEREDOC
puts text # Here-documents (<<EOF) require file descriptors and are not
# available in the browser WASM runtime.
# Standard usage:
# cat <<EOF
# Dear $name,
# Your order #$order_id has shipped.
# Thanks!
# EOF Here-documents (
<) feed a multiline string to a command's standard input, with variable expansion. Using <<-EOF strips leading tabs. Using <<'EOF' (quoted) suppresses variable expansion (like Ruby's <<~'HEREDOC'). File descriptor support is required, so here-docs are not runnable in this browser runtime.