HackerRank Shell
Intro
Unless otherwise noted, assume all scripts contain the following shebang:
#!/usr/bin/env bash
Easy Challenges
Let’s Echo
Tags: #cmdline #shell #bash #echo #printf Links: challenge
$ echo HELLO
$ printf '%s\n' HELLO
Looping With Numbers
-
Tags: #cmdline #shell #bash #numbers #looping #for
-
Links: challenge
for (( i = 1; i <= 9; ++i ))
do
echo "$i"
done
Or using ranges:
$ printf '%d\n' {1..50}
Looping And Skipping
-
Tags: #cmdline, #numbers #looping #for
-
Links: challenge
for (( i = 1; i <= 9; ++i ))
do
if (( i % 2 == 0 ))
then
continue
fi
echo "$i"
done
$ bash script.sh
1
3
5
7
9
Could also use ‘’echo:’’
$ echo -ne {1..9..2} '\n'
The -e
option is to enable some escapes. help echo
for more.
Or using seq
:
$ seq -s ' ' 1 2 9
A Personalized Echo
-
Tags: #cmdline #read #echo
-
Links: challenge
$ read -r name
$ printf 'Welcome %s\n' "$name"
The World of Numbers
-
Tags: #cmdline #shell #bash #numbers #math #bc #ranges
-
Links: challenge
First, see this clever use of range to produce the math expressions:
$ read -r x y
8 2
$ printf '%s\n' "$x"{+,-,*,/}"$y"
8+2
8-2
8*2
8/2
Then, feed those expressions to bc
:
$ read -r x y
8 2
$ printf '%s\n' "scale=2; $x"{+,-,*,/}"$y" | bc
10
6
16
4.00
If y
is negative, like -2
we would receive an error:
$ read -r x y
5 -2
$ printf '%s\n' "scale=2; $x"{+,-,*,/}"$y" | bc
3
(standard_in) 2: syntax error
-10
-2.50
Adding parenthesis prevents the error, because our expression would be
like 5—2
, but 5-(-2)
is OK with bc
:
$ read -r x y
5 -2
$ printf '%s\n' "scale=2; $x"{+,-,*,/}"($y)" | bc
3
7
-10
-2.50
Or something more manual and verbose:
read x </dev/stdin
read y </dev/stdin
printf '%d\n' $(( x + y ))
printf '%d\n' $(( x - y ))
printf '%d\n' $(( x * y ))
printf '%d\n' $(( x / y ))
The challenge wants integer division, so, we simply omit bc ’s
scale special variable.
|
read -r answer
case "$answer" in
[Yy]*)
printf '%s\n' YES
;;
[Nn]*)
printf '%s\n' NO
;;
*)
printf '%s\n' 'What the poop‽ 💩'
;;
esac
$ bash script.sh
yes
YES
$ bash script.sh
Y
YES
$ bash script.sh
n
NO
$ bash script.sh
lol
What the poop‽ 💩
Getting started with conditionals
-
Tags: #cmdline #shell #bash #conditionals
-
Links: challenge
read -r answer
case "$answer" in
[Yy]*)
printf '%s\n' YES
;;
[Nn]*)
printf '%s\n' NO
;;
*)
printf '%s\n' 'What the poop‽ 💩'
;;
esac
$ bash script.sh
yes
YES
$ bash script.sh
Y
YES
$ bash script.sh
n
NO
$ bash script.sh
lol
What the poop‽ 💩
More on Conditionals
-
Tags: #cmdline #shell #bash #conditionals #math
-
Links: challenge
Solution based on side lengths.
-
equilateral: x == y && y == z
-
scalene: x != y && y != z && z != x
-
isosceles: any other
read -r x
read -r y
read -r z
[[ "$x" == "$y" ]] && [[ "$y" == "$z" ]] && echo EQUILATERAL && exit 0
[[ "$x" != "$y" ]] && [[ "$y" != "$z" ]] && [[ "$z" != "$x" ]] && echo SCALENE && exit 0
echo ISOSCELES && exit 0
Arithmetic Operations
-
Tags: #cmdline #shell #bash #math #bc
-
Links: challenge
expression="$1"
printf '%.3f\n' "$(echo "$expression" | bc -l)"
bc -l
produces up to 6 decimal places. If we use bc
scale to 3, for
instance, depending on the result, we would produce wrong results
because printf %f
format specifier does rounding by itself.
bc
scale is 0 by default if not explicitly set. Also, bc
does no
rounding.
printf
rounds up from 6, and down from 5:
$ printf '%.3f\n' 1.2583
1.258
$ printf '%.3f\n' 1.2585
1.258
$ printf '%.3f\n' 1.2586
1.259
Only when the number after 8 passes 5, that is, 6 and above, is that the
number is rounded up to 1.259. If one uses scale=3
in bc
, then it
truncates (does not round) to three decimal places and printf
has no
way to round up, making the solution to the exercise incorrect.
Therefore, we use bc -l
without scale, or use scale=4
at least.
Compute the Average
-
Tags: #cmdline #shell #bash #math
-
Links: challenge
read -r n
sum=0
if [[ "$n" == 0 ]]
then
printf '%.3f\n' "$(echo 'scale=4; 0' | bc -l)"
exit 0
fi
for ((i = 0; i < n; ++i))
do
read -r x
sum=$((sum + x))
done
printf '%.3f\n' "$(echo "scale=4; $sum / $n" | bc -l)"
We used scale=4
by the same reasons described earlier about truncating
and rounding.
cut Challenges
-
Tags: #cmdline #shell #bash #cut
$ cut -b 3 -
$ cut -b 2,7 -
$ cut -b 2-7 -
$ cut -b 1-4 -
$ cut -d $'\t' -f 1,2,3 -
$ cut -c 13- -
$ cut -d ' ' -f 4 -
$ cut -d ' ' -f 1,2,3 -
$ cut -d $'\t' -f 2- -
Head of Text File Challenges
$ head -n 20
$ head -c 20
Tail of a Text File 1 and 2
-
Tags: #cmdline #shell #bash #tail
-
Links: challenge
$ tail -n 20 -
$ tail -c 20 -
tr Command 1
-
Tags: #cmdline #shell #bash #tr #here-document #assignment
-
Links: challenge
# Assign some text to the variable `input'.
$ read -r -d '' input << 'EOF'
int i = (int) 5.8;
int res = (23 + i) * 2;
EOF
# Inspect `input' contents.
$ echo "$input"
int i = (int) 5.8;
int res = (23 + i) * 2;
# Apply `tr' to `input' and see ( and ) replaced with [ and ].
$ echo "$input" | tr '()' '[]'
int i = [int] 5.8;
int res = [23 + i] * 2;
A Here
Document is used to assign lines of text to the variable input
.
sort Lines Challenges
-
Tags: #cmdline #shell #bash #sort
-
Links: challenge
$ echo -e 'aa\nbb\naa\ncc\nff\ncc' | sort -
aa
aa
bb
cc
cc
ff
$ echo -e 'aa\nbb\naa\ncc\nff\ncc' | sort -r -
ff
cc
cc
bb
aa
aa
$ echo -e '2.1\n3\n0.2\n0' | sort -n -
0
0.2
2.1
3
$ echo -e '2.1\n3\n0.2\n0' | sort -nr -
3
2.1
0.2
0
# Sort by field 2, taking Tab as field separator.
$ sort -t $'\t' -nr -k 2 -
# Same, but in ascending order.
$ sort -t $'\t' -n -k 2 -
# This time the delimiter is a “|” character
$ sort -t '|' -nr -k 2 -
uniq Challenges
-
Tags: #cmdline #shell #bash #uniq
-
Links: challenge
$ uniq -
```
Display the count of lines that were uniqfied and the uniqfied lines without leading whitespace/tabs:
$ read -r -d '' lines << 'EOF'
> foo
> foo
> bar
> bar
> bar
> tux
> EOF
$ echo "$lines" | uniq -c - | sed 's/ \+\([0-9]\+ [^ ]\+\)/\1/'
2 foo
3 bar
1 tux
$ echo "$lines" | uniq -c - | sed 's/^[[:space:]]*//g'
2 foo
3 bar
1 tux
$ echo "$lines" | uniq -c - | cut -b 7- -
2 foo
3 bar
1 tux
$ echo "$lines" | uniq -c - | xargs -l
2 foo
3 bar
1 tux
$ echo "$lines" | uniq -c - | xargs -L 1
2 foo
3 bar
1 tux
$ echo "$lines" | uniq -c - | colrm 1 6
2 foo
3 bar
1 tux
# Case Insenstivie.
$ read -r -d '' lines << 'EOF'
> FoO
> fOO
> baR
> Bar
> bAr
> TUX
> EOF
$ echo "$lines" | uniq -ci - | cut -b 7- -
2 FoO
3 baR
1 TUX
$ echo "$lines" | uniq -u -
TUX
Read In An Array
-
Tags: #cmdline #shell #bash #arrays
-
Links: challenge
$ arr=()
$ while read -r line ; do arr+=("$line") ; done < /dev/stdin
$ echo "${#arr[*]}"
Display an Element of an Array
-
Tags: #cmdline #shell #bash #arrays
-
Links: challenge
mapfile -t countries
echo "${countries[3]}"
-t
in mapfile
removes the trailing delimiter so the array elements
are “clean”.
Count Elements in an Array
-
Tags: #cmdline #shell #bash #arrays
-
Links: challenge
mapfile -t countries
echo "${#countries[@]}"
Slice An Array
-
Tags: #cmdline #shell #bash #arrays
-
Links: challenge
Print the array with the syntax ${arr[*]:OFFSET:LENGTH}
.
$ read -r -d '' countries << 'EOF'
> Namibia
> Nauru
> Nepal
> Netherlands
> NewZealand
> Nicaragua
> Niger
> Nigeria
> NorthKorea
> Norway
> EOF
$ echo "${arr[*]:3:5}"
Netherlands NewZealand Nicaragua Niger Nigeria NorthKorea Norway
Could read with countries=($(cat))
too, but ShellSheck complains.
Either use the read
as above, or with mapfile -t arr
.
Other options would be:
paste -d ' ' -s | cut -d ' ' -f4-8 -
and:
head -8 | tail -5 | paste -s -d ' ' -
Concatenate Array With Itself
-
Tags: #cmdline #shell #bash #arrays
-
Links: challenge
mapfile -t countries
countries+=("${countries[@]}" "${countries[@]}")
echo "${countries[*]}"
Medium Challenges
grep challenges
-
Tags: #cmdline #shell #grep
-
Links: challenge1, challenge2, challenge3
$ grep '\<the\>'
$ grep -i '\<the\>'
$ grep -iv '\<that\>'
awk challenges
-
Tags: #cmdline #shell #awk
-
Links: challenge 1, challenge 2, challenge 3, challenge 4
Challenge 1:
$ awk '{ if ($4 == "") print "Not all scores are available for " $1 }'
Challenge 2:
awk '{
answer[0] = "Fail";
answer[1] = "Pass";
print $1, ":", answer[$2 >= 50 && $3 >= 50 && $4 >= 50];
}'
Challenge 3:
awk '{
avg=($2 + $3 + $4) / 3
if (avg >= 80)
print $0 " : A";
else if (avg >= 60)
print $0 " : B";
else
print $0 " : FAIL";
}'
Challenge 4:
awk 'ORS=NR % 2 ? ";" : "\n"'
Filter an Array With Patterns
-
Tags: #cmdline #shell #bash #arrays #pattern-matching
-
Links: challenge
while read -r line ; do
if [[ ! "$line" =~ [Aa] ]]
then
echo "$line"
fi
done
Remove First Capital Letter From Each Array Element
-
Tags: #cmdline #shell #bash #arrays #pattern-matching
-
Links: challenge
arr=()
while read -r line ; do
arr+=("${line/[A-Z]/.}")
done
echo "${arr[*]}"
Hard Challenges
sed 5
-
Tags: #cmdline #shell #sed
-
Links: challenge
sed 's/\([0-9]\+\) \([0-9]\+\) \([0-9]\+\) \([0-9]\+\)/\4 \3 \2 \1/'
Backreferences in the search pattern mean they match the same
chars, not the same general regex. That is, (.)o(.) matches “bob” or
“bob”, for instance, but not “bop”. If (.) matched “x”, then \1 in
the search must also match an “x”. That is why we can’t do
s/\([0-9]\+\) \1 \1 \1 , because it would only match if all four fields
of the number were the same thing, like “1234 1234 1234 1234”.
|
Lonely Integer
-
Tags: #cmdline #shell #bash #numbers
-
Links: challenge
Not very elegant, but makes use of arrays, which is what they ask for.
#!/usr/bin/env bash
#
# This solution uses a histogram-like approach.
#
# Dummy-read, since we don't need the first argument they
# feed into the input.
read -r
# Read input numbers.
read -r -a nums
# An array to keep track of which numbers appeared how many times.
declare -A hist
for n in "${nums[@]}"
do
if [[ -z "${hist[$n]}" ]]
then
# Use the number as index and increment that index and
# initialize it to 1.
hist[$n]=1
else
# Increment it each time that number appears.
hist[$n]=$((${hist[$n]} + 1))
fi
done
# Iterate over the indexes.
for idx in "${!hist[@]}"
do
# If that number appeared only once...
if (( hist[$idx] == 1 ))
then
# ...then print it and bail out.
echo "$idx"
break;
fi
done
Fractal Tree
-
Tags: #cmdline #shell #bash
-
Links: challenge
#!/usr/bin/env bash
#
# Invoke it like this:
#
# bash script.sh 5
#
declare -A grid
rows=63
cols=100
#
# Initialize the 63x100 grid with underscores.
#
init () {
for (( row = 0; row < rows; ++row ))
do
for (( col = 0; col < cols; ++col ))
do
grid[$row,$col]=_
done
done
}
#
# Actually treeify the drawing.
#
treeify () {
local count=$1
local row=$2
local col=$3
local iteration=$4
for (( i = 0; i < count; ++i ))
do
grid[$row,$col]=1
(( row -= 1 ))
done
for (( i = 0; i < count; i++ ))
do
grid[$row,$((col - i - 1))]=1
grid[$row,$((col + i + 1))]=1
(( row -= 1 ))
done
if (( iteration > 1 ))
then
treeify $(( count >> 1 )) "$row" $(( col - count )) $(( iteration - 1 ))
treeify $(( count >> 1 )) "$row" $(( col + count )) $(( iteration - 1 ))
fi
}
#
# Simply output the grid, already treeified, to the screen.
#
display () {
for (( row = 0 ; row < rows ; ++row ))
do
for (( col = 0 ; col < cols ; ++col ))
do
printf '%s' "${grid[$row,$col]}"
done
printf '\n'
done
}
initial_count=16
initial_row=62
initial_col=49
iterations="${1:-5}"
if (( 1 > iterations || iterations > 5 ))
then
printf '%s\n' 'Provide a number between 1 and 5, please.' 1>&2
else
init
treeify "$initial_count" "$initial_row" "$initial_col" "$iterations"
display
fi