Solution to tournament in Bash

This commit is contained in:
David Doblas Jiménez 2023-10-07 15:09:46 +02:00
commit 5c52e8e34d
8 changed files with 1268 additions and 0 deletions

View File

@ -0,0 +1,25 @@
{
"authors": [
"glennj"
],
"contributors": [
"bkhl",
"guygastineau",
"IsaacG",
"kotp"
],
"files": {
"solution": [
"grep.sh"
],
"test": [
"grep.bats"
],
"example": [
".meta/example.sh"
]
},
"blurb": "Search a file for lines matching a regular expression pattern. Return the line number and contents of each matching line.",
"source": "Conversation with Nate Foster.",
"source_url": "https://www.cs.cornell.edu/Courses/cs3110/2014sp/hw/0/ps0.pdf"
}

View File

@ -0,0 +1 @@
{"track":"bash","exercise":"grep","id":"f17326cb951d4357a2394a24d217a5f3","url":"https://exercism.org/tracks/bash/exercises/grep","handle":"Kimawari","is_requester":true,"auto_approve":false}

105
bash/grep/HELP.md Normal file
View File

@ -0,0 +1,105 @@
# Help
## Running the tests
Each exercise contains a test file.
Run the tests using the `bats` program.
```bash
bats hello_world.bats
```
`bats` will need to be installed.
See the [Testing on the Bash track][tests] page for instructions to install `bats` for your system.
[tests]: https://exercism.org/docs/tracks/bash/tests
## Help for assert functions
The tests use functions from the [bats-assert][bats-assert] library.
Help for the various `assert*` functions can be found there.
[bats-assert]: https://github.com/bats-core/bats-assert
## Skipped tests
Solving an exercise means making all its tests pass.
By default, only one test (the first one) is executed when you run the tests.
This is intentional, as it allows you to focus on just making that one test pass.
Once it passes, you can enable the next test by commenting out or removing the next annotation:
```bash
[[ $BATS_RUN_SKIPPED == true ]] || skip
```
## Overriding skips
To run all tests, including the ones with `skip` annotations, you can run:
```bash
BATS_RUN_SKIPPED=true bats exercise_name.bats
```
It can be convenient to use a wrapper function to save on typing:
```bash
bats() {
BATS_RUN_SKIPPED=true command bats *.bats
}
```
Then run tests with just:
```bash
bats
```
## Submitting your solution
You can submit your solution using the `exercism submit grep.sh` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Bash track's documentation](https://exercism.org/docs/tracks/bash)
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
Check your code for syntax errors: paste your code into
[https://shellcheck.net](https://shellcheck.net) (or [install it](https://github.com/koalaman/shellcheck#user-content-installing) on your machine).
Stack Overflow will be your first stop for bash questions.
* start with the [`bash` tag](https://stackoverflow.com/questions/tagged/bash) to search for your specific question: it's probably already been asked
* under the bash tag on Stackoverflow, the [Learn more...](https://stackoverflow.com/tags/bash/info) link has _tons_ of good information.
* the "Books and Resources" section is particularly useful.
* the [`bash` tag](https://unix.stackexchange.com/questions/tagged/bash) on Unix & Linux is also active
## External utilities
`bash` is a language to write "scripts" -- programs that can call
external tools, such as
[`sed`](https://www.gnu.org/software/sed/),
[`awk`](https://www.gnu.org/software/gawk/),
[`date`](https://www.gnu.org/software/coreutils/manual/html_node/date-invocation.html)
and even programs written in other programming languages,
like [`Python`](https://www.python.org/).
This track does not restrict the usage of these utilities, and as long
as your solution is portable between systems and does not require
installation of third party applications, feel free to use them to solve
the exercise.
For an extra challenge, if you would like to have a better understanding of
the language, try to re-implement the solution in pure bash, without using
any external tools. There are some types of problems that bash cannot solve,
such as floating point arithmetic and manipulating dates: for those, you
must call out to an external tool.

65
bash/grep/README.md Normal file
View File

@ -0,0 +1,65 @@
# Grep
Welcome to Grep on Exercism's Bash Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
## Instructions
Search files for lines matching a search string and return all matching lines.
The Unix [`grep`][grep] command searches files for lines that match a regular expression.
Your task is to implement a simplified `grep` command, which supports searching for fixed strings.
The `grep` command takes three arguments:
1. The string to search for.
2. Zero or more flags for customizing the command's behavior.
3. One or more files to search in.
It then reads the contents of the specified files (in the order specified), finds the lines that contain the search string, and finally returns those lines in the order in which they were found.
When searching in multiple files, each matching line is prepended by the file name and a colon (':').
## Flags
The `grep` command supports the following flags:
- `-n` Prepend the line number and a colon (':') to each line in the output, placing the number after the filename (if present).
- `-l` Output only the names of the files that contain at least one matching line.
- `-i` Match using a case-insensitive comparison.
- `-v` Invert the program -- collect all lines that fail to match.
- `-x` Search only for lines where the search string matches the entire line.
[grep]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/grep.html
## To `grep` or not to `grep`, that is the question
Although this exercise can be trivially solved by simply passing the
arguments to `grep`, implement this exercise using bash only. The aim
of this exercism track is to learn how to use bash builtin commands to solve
problems.
To solve this exercise, you'll need to:
* parse command line arguments: [`getopts`](https://stackoverflow.com/tags/getopts/info) is useful for this.
* iterate over the lines of a file: this is [bash FAQ #1](https://mywiki.wooledge.org/BashFAQ/001)
* use regular expression matching: bash can do this using the `=~` operator
within [`[[ ... ]]`](https://www.gnu.org/software/bash/manual/bash.html#index-_005b_005b)
---
## Source
### Created by
- @glennj
### Contributed to by
- @bkhl
- @guygastineau
- @IsaacG
- @kotp
### Based on
Conversation with Nate Foster. - https://www.cs.cornell.edu/Courses/cs3110/2014sp/hw/0/ps0.pdf

637
bash/grep/bats-extra.bash Normal file
View File

@ -0,0 +1,637 @@
# This is the source code for bats-support and bats-assert, concatenated
# * https://github.com/bats-core/bats-support
# * https://github.com/bats-core/bats-assert
#
# Comments have been removed to save space. See the git repos for full source code.
############################################################
#
# bats-support - Supporting library for Bats test helpers
#
# Written in 2016 by Zoltan Tombol <zoltan dot tombol at gmail dot com>
#
# To the extent possible under law, the author(s) have dedicated all
# copyright and related and neighboring rights to this software to the
# public domain worldwide. This software is distributed without any
# warranty.
#
# You should have received a copy of the CC0 Public Domain Dedication
# along with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.
#
fail() {
(( $# == 0 )) && batslib_err || batslib_err "$@"
return 1
}
batslib_is_caller() {
local -i is_mode_direct=1
# Handle options.
while (( $# > 0 )); do
case "$1" in
-i|--indirect) is_mode_direct=0; shift ;;
--) shift; break ;;
*) break ;;
esac
done
# Arguments.
local -r func="$1"
# Check call stack.
if (( is_mode_direct )); then
[[ $func == "${FUNCNAME[2]}" ]] && return 0
else
local -i depth
for (( depth=2; depth<${#FUNCNAME[@]}; ++depth )); do
[[ $func == "${FUNCNAME[$depth]}" ]] && return 0
done
fi
return 1
}
batslib_err() {
{ if (( $# > 0 )); then
echo "$@"
else
cat -
fi
} >&2
}
batslib_count_lines() {
local -i n_lines=0
local line
while IFS='' read -r line || [[ -n $line ]]; do
(( ++n_lines ))
done < <(printf '%s' "$1")
echo "$n_lines"
}
batslib_is_single_line() {
for string in "$@"; do
(( $(batslib_count_lines "$string") > 1 )) && return 1
done
return 0
}
batslib_get_max_single_line_key_width() {
local -i max_len=-1
while (( $# != 0 )); do
local -i key_len="${#1}"
batslib_is_single_line "$2" && (( key_len > max_len )) && max_len="$key_len"
shift 2
done
echo "$max_len"
}
batslib_print_kv_single() {
local -ir col_width="$1"; shift
while (( $# != 0 )); do
printf '%-*s : %s\n' "$col_width" "$1" "$2"
shift 2
done
}
batslib_print_kv_multi() {
while (( $# != 0 )); do
printf '%s (%d lines):\n' "$1" "$( batslib_count_lines "$2" )"
printf '%s\n' "$2"
shift 2
done
}
batslib_print_kv_single_or_multi() {
local -ir width="$1"; shift
local -a pairs=( "$@" )
local -a values=()
local -i i
for (( i=1; i < ${#pairs[@]}; i+=2 )); do
values+=( "${pairs[$i]}" )
done
if batslib_is_single_line "${values[@]}"; then
batslib_print_kv_single "$width" "${pairs[@]}"
else
local -i i
for (( i=1; i < ${#pairs[@]}; i+=2 )); do
pairs[$i]="$( batslib_prefix < <(printf '%s' "${pairs[$i]}") )"
done
batslib_print_kv_multi "${pairs[@]}"
fi
}
batslib_prefix() {
local -r prefix="${1:- }"
local line
while IFS='' read -r line || [[ -n $line ]]; do
printf '%s%s\n' "$prefix" "$line"
done
}
batslib_mark() {
local -r symbol="$1"; shift
# Sort line numbers.
set -- $( sort -nu <<< "$( printf '%d\n' "$@" )" )
local line
local -i idx=0
while IFS='' read -r line || [[ -n $line ]]; do
if (( ${1:--1} == idx )); then
printf '%s\n' "${symbol}${line:${#symbol}}"
shift
else
printf '%s\n' "$line"
fi
(( ++idx ))
done
}
batslib_decorate() {
echo
echo "-- $1 --"
cat -
echo '--'
echo
}
############################################################
assert() {
if ! "$@"; then
batslib_print_kv_single 10 'expression' "$*" \
| batslib_decorate 'assertion failed' \
| fail
fi
}
assert_equal() {
if [[ $1 != "$2" ]]; then
batslib_print_kv_single_or_multi 8 \
'expected' "$2" \
'actual' "$1" \
| batslib_decorate 'values do not equal' \
| fail
fi
}
assert_failure() {
: "${output?}"
: "${status?}"
(( $# > 0 )) && local -r expected="$1"
if (( status == 0 )); then
batslib_print_kv_single_or_multi 6 'output' "$output" \
| batslib_decorate 'command succeeded, but it was expected to fail' \
| fail
elif (( $# > 0 )) && (( status != expected )); then
{ local -ir width=8
batslib_print_kv_single "$width" \
'expected' "$expected" \
'actual' "$status"
batslib_print_kv_single_or_multi "$width" \
'output' "$output"
} \
| batslib_decorate 'command failed as expected, but status differs' \
| fail
fi
}
assert_line() {
local -i is_match_line=0
local -i is_mode_partial=0
local -i is_mode_regexp=0
: "${lines?}"
# Handle options.
while (( $# > 0 )); do
case "$1" in
-n|--index)
if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
echo "\`--index' requires an integer argument: \`$2'" \
| batslib_decorate 'ERROR: assert_line' \
| fail
return $?
fi
is_match_line=1
local -ri idx="$2"
shift 2
;;
-p|--partial) is_mode_partial=1; shift ;;
-e|--regexp) is_mode_regexp=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
if (( is_mode_partial )) && (( is_mode_regexp )); then
echo "\`--partial' and \`--regexp' are mutually exclusive" \
| batslib_decorate 'ERROR: assert_line' \
| fail
return $?
fi
# Arguments.
local -r expected="$1"
if (( is_mode_regexp == 1 )) && [[ '' =~ $expected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$expected'" \
| batslib_decorate 'ERROR: assert_line' \
| fail
return $?
fi
# Matching.
if (( is_match_line )); then
# Specific line.
if (( is_mode_regexp )); then
if ! [[ ${lines[$idx]} =~ $expected ]]; then
batslib_print_kv_single 6 \
'index' "$idx" \
'regexp' "$expected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'regular expression does not match line' \
| fail
fi
elif (( is_mode_partial )); then
if [[ ${lines[$idx]} != *"$expected"* ]]; then
batslib_print_kv_single 9 \
'index' "$idx" \
'substring' "$expected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line does not contain substring' \
| fail
fi
else
if [[ ${lines[$idx]} != "$expected" ]]; then
batslib_print_kv_single 8 \
'index' "$idx" \
'expected' "$expected" \
'actual' "${lines[$idx]}" \
| batslib_decorate 'line differs' \
| fail
fi
fi
else
# Contained in output.
if (( is_mode_regexp )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} =~ $expected ]] && return 0
done
{ local -ar single=( 'regexp' "$expected" )
local -ar may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} \
| batslib_decorate 'no output line matches regular expression' \
| fail
elif (( is_mode_partial )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} == *"$expected"* ]] && return 0
done
{ local -ar single=( 'substring' "$expected" )
local -ar may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} \
| batslib_decorate 'no output line contains substring' \
| fail
else
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
[[ ${lines[$idx]} == "$expected" ]] && return 0
done
{ local -ar single=( 'line' "$expected" )
local -ar may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
batslib_print_kv_single_or_multi "$width" "${may_be_multi[@]}"
} \
| batslib_decorate 'output does not contain line' \
| fail
fi
fi
}
assert_output() {
local -i is_mode_partial=0
local -i is_mode_regexp=0
local -i is_mode_nonempty=0
local -i use_stdin=0
: "${output?}"
# Handle options.
if (( $# == 0 )); then
is_mode_nonempty=1
fi
while (( $# > 0 )); do
case "$1" in
-p|--partial) is_mode_partial=1; shift ;;
-e|--regexp) is_mode_regexp=1; shift ;;
-|--stdin) use_stdin=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
if (( is_mode_partial )) && (( is_mode_regexp )); then
echo "\`--partial' and \`--regexp' are mutually exclusive" \
| batslib_decorate 'ERROR: assert_output' \
| fail
return $?
fi
# Arguments.
local expected
if (( use_stdin )); then
expected="$(cat -)"
else
expected="${1-}"
fi
# Matching.
if (( is_mode_nonempty )); then
if [ -z "$output" ]; then
echo 'expected non-empty output, but output was empty' \
| batslib_decorate 'no output' \
| fail
fi
elif (( is_mode_regexp )); then
if [[ '' =~ $expected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$expected'" \
| batslib_decorate 'ERROR: assert_output' \
| fail
elif ! [[ $output =~ $expected ]]; then
batslib_print_kv_single_or_multi 6 \
'regexp' "$expected" \
'output' "$output" \
| batslib_decorate 'regular expression does not match output' \
| fail
fi
elif (( is_mode_partial )); then
if [[ $output != *"$expected"* ]]; then
batslib_print_kv_single_or_multi 9 \
'substring' "$expected" \
'output' "$output" \
| batslib_decorate 'output does not contain substring' \
| fail
fi
else
if [[ $output != "$expected" ]]; then
batslib_print_kv_single_or_multi 8 \
'expected' "$expected" \
'actual' "$output" \
| batslib_decorate 'output differs' \
| fail
fi
fi
}
assert_success() {
: "${output?}"
: "${status?}"
if (( status != 0 )); then
{ local -ir width=6
batslib_print_kv_single "$width" 'status' "$status"
batslib_print_kv_single_or_multi "$width" 'output' "$output"
} \
| batslib_decorate 'command failed' \
| fail
fi
}
refute() {
if "$@"; then
batslib_print_kv_single 10 'expression' "$*" \
| batslib_decorate 'assertion succeeded, but it was expected to fail' \
| fail
fi
}
refute_line() {
local -i is_match_line=0
local -i is_mode_partial=0
local -i is_mode_regexp=0
: "${lines?}"
# Handle options.
while (( $# > 0 )); do
case "$1" in
-n|--index)
if (( $# < 2 )) || ! [[ $2 =~ ^([0-9]|[1-9][0-9]+)$ ]]; then
echo "\`--index' requires an integer argument: \`$2'" \
| batslib_decorate 'ERROR: refute_line' \
| fail
return $?
fi
is_match_line=1
local -ri idx="$2"
shift 2
;;
-p|--partial) is_mode_partial=1; shift ;;
-e|--regexp) is_mode_regexp=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
if (( is_mode_partial )) && (( is_mode_regexp )); then
echo "\`--partial' and \`--regexp' are mutually exclusive" \
| batslib_decorate 'ERROR: refute_line' \
| fail
return $?
fi
# Arguments.
local -r unexpected="$1"
if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$unexpected'" \
| batslib_decorate 'ERROR: refute_line' \
| fail
return $?
fi
# Matching.
if (( is_match_line )); then
# Specific line.
if (( is_mode_regexp )); then
if [[ ${lines[$idx]} =~ $unexpected ]]; then
batslib_print_kv_single 6 \
'index' "$idx" \
'regexp' "$unexpected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'regular expression should not match line' \
| fail
fi
elif (( is_mode_partial )); then
if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
batslib_print_kv_single 9 \
'index' "$idx" \
'substring' "$unexpected" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line should not contain substring' \
| fail
fi
else
if [[ ${lines[$idx]} == "$unexpected" ]]; then
batslib_print_kv_single 5 \
'index' "$idx" \
'line' "${lines[$idx]}" \
| batslib_decorate 'line should differ' \
| fail
fi
fi
else
# Line contained in output.
if (( is_mode_regexp )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} =~ $unexpected ]]; then
{ local -ar single=( 'regexp' "$unexpected" 'index' "$idx" )
local -a may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} \
| batslib_decorate 'no line should match the regular expression' \
| fail
return $?
fi
done
elif (( is_mode_partial )); then
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} == *"$unexpected"* ]]; then
{ local -ar single=( 'substring' "$unexpected" 'index' "$idx" )
local -a may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} \
| batslib_decorate 'no line should contain substring' \
| fail
return $?
fi
done
else
local -i idx
for (( idx = 0; idx < ${#lines[@]}; ++idx )); do
if [[ ${lines[$idx]} == "$unexpected" ]]; then
{ local -ar single=( 'line' "$unexpected" 'index' "$idx" )
local -a may_be_multi=( 'output' "$output" )
local -ir width="$( batslib_get_max_single_line_key_width "${single[@]}" "${may_be_multi[@]}" )"
batslib_print_kv_single "$width" "${single[@]}"
if batslib_is_single_line "${may_be_multi[1]}"; then
batslib_print_kv_single "$width" "${may_be_multi[@]}"
else
may_be_multi[1]="$( printf '%s' "${may_be_multi[1]}" | batslib_prefix | batslib_mark '>' "$idx" )"
batslib_print_kv_multi "${may_be_multi[@]}"
fi
} \
| batslib_decorate 'line should not be in output' \
| fail
return $?
fi
done
fi
fi
}
refute_output() {
local -i is_mode_partial=0
local -i is_mode_regexp=0
local -i is_mode_empty=0
local -i use_stdin=0
: "${output?}"
# Handle options.
if (( $# == 0 )); then
is_mode_empty=1
fi
while (( $# > 0 )); do
case "$1" in
-p|--partial) is_mode_partial=1; shift ;;
-e|--regexp) is_mode_regexp=1; shift ;;
-|--stdin) use_stdin=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
if (( is_mode_partial )) && (( is_mode_regexp )); then
echo "\`--partial' and \`--regexp' are mutually exclusive" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
# Arguments.
local unexpected
if (( use_stdin )); then
unexpected="$(cat -)"
else
unexpected="${1-}"
fi
if (( is_mode_regexp == 1 )) && [[ '' =~ $unexpected ]] || (( $? == 2 )); then
echo "Invalid extended regular expression: \`$unexpected'" \
| batslib_decorate 'ERROR: refute_output' \
| fail
return $?
fi
# Matching.
if (( is_mode_empty )); then
if [ -n "$output" ]; then
batslib_print_kv_single_or_multi 6 \
'output' "$output" \
| batslib_decorate 'output non-empty, but expected no output' \
| fail
fi
elif (( is_mode_regexp )); then
if [[ $output =~ $unexpected ]]; then
batslib_print_kv_single_or_multi 6 \
'regexp' "$unexpected" \
'output' "$output" \
| batslib_decorate 'regular expression should not match output' \
| fail
fi
elif (( is_mode_partial )); then
if [[ $output == *"$unexpected"* ]]; then
batslib_print_kv_single_or_multi 9 \
'substring' "$unexpected" \
'output' "$output" \
| batslib_decorate 'output should not contain substring' \
| fail
fi
else
if [[ $output == "$unexpected" ]]; then
batslib_print_kv_single_or_multi 6 \
'output' "$output" \
| batslib_decorate 'output equals, but it was expected to differ' \
| fail
fi
fi
}

377
bash/grep/grep.bats Normal file
View File

@ -0,0 +1,377 @@
#!/usr/bin/env bats
load bats-extra
# local version: 1.2.0.0
setup() {
cat > iliad.txt << END_ILIAD
Achilles sing, O Goddess! Peleus' son;
His wrath pernicious, who ten thousand woes
Caused to Achaia's host, sent many a soul
Illustrious into Ades premature,
And Heroes gave (so stood the will of Jove)
To dogs and to all ravening fowls a prey,
When fierce dispute had separated once
The noble Chief Achilles from the son
Of Atreus, Agamemnon, King of men.
END_ILIAD
cat > midsummer-night.txt << END_MIDSUMMER
I do entreat your grace to pardon me.
I know not by what power I am made bold,
Nor how it may concern my modesty,
In such a presence here to plead my thoughts;
But I beseech your grace that I may know
The worst that may befall me in this case,
If I refuse to wed Demetrius.
END_MIDSUMMER
cat > paradise-lost.txt << END_PARADISE
Of Mans First Disobedience, and the Fruit
Of that Forbidden Tree, whose mortal tast
Brought Death into the World, and all our woe,
With loss of Eden, till one greater Man
Restore us, and regain the blissful Seat,
Sing Heav'nly Muse, that on the secret top
Of Oreb, or of Sinai, didst inspire
That Shepherd, who first taught the chosen Seed
END_PARADISE
}
teardown() {
rm iliad.txt midsummer-night.txt paradise-lost.txt
}
# Test grepping a single file
@test "One file, one match, no flags" {
#[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="Of Atreus, Agamemnon, King of men."
pattern="Agamemnon"
flags=()
files=(iliad.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, one match, print line numbers flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="2:Of that Forbidden Tree, whose mortal tast"
pattern="Forbidden"
flags=(-n)
files=(paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, one match, case-insensitive flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="Of that Forbidden Tree, whose mortal tast"
pattern="FORBIDDEN"
flags=(-i)
files=(paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, one match, print file names flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="paradise-lost.txt"
pattern="Forbidden"
flags=(-l)
files=(paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, one match, match entire lines flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="With loss of Eden, till one greater Man"
pattern="With loss of Eden, till one greater Man"
flags=(-x)
files=(paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, one match, multiple flags" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="9:Of Atreus, Agamemnon, King of men."
pattern="OF ATREUS, Agamemnon, KIng of MEN."
flags=(-n -i -x)
files=(iliad.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, several matches, no flags" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="Nor how it may concern my modesty,
But I beseech your grace that I may know
The worst that may befall me in this case,"
pattern="may"
flags=()
files=(midsummer-night.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, several matches, print line numbers flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="3:Nor how it may concern my modesty,
5:But I beseech your grace that I may know
6:The worst that may befall me in this case,"
pattern="may"
flags=(-n)
files=(midsummer-night.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, several matches, match entire lines flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected=""
pattern="may"
flags=(-x)
files=(midsummer-night.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, several matches, case-insensitive flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="Achilles sing, O Goddess! Peleus' son;
The noble Chief Achilles from the son"
pattern="ACHILLES"
flags=(-i)
files=(iliad.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, several matches, inverted flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="Brought Death into the World, and all our woe,
With loss of Eden, till one greater Man
Restore us, and regain the blissful Seat,
Sing Heav'nly Muse, that on the secret top
That Shepherd, who first taught the chosen Seed"
pattern="Of"
flags=(-v)
files=(paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, no matches, various flags" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected=""
pattern="Gandalf"
flags=(-n -l -x -i)
files=(iliad.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, one match, file flag takes precedence over line flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="iliad.txt"
pattern="ten"
flags=(-n -l)
files=(iliad.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "One file, several matches, inverted and match entire lines flags" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="Achilles sing, O Goddess! Peleus' son;
His wrath pernicious, who ten thousand woes
Caused to Achaia's host, sent many a soul
And Heroes gave (so stood the will of Jove)
To dogs and to all ravening fowls a prey,
When fierce dispute had separated once
The noble Chief Achilles from the son
Of Atreus, Agamemnon, King of men."
pattern="Illustrious into Ades premature,"
flags=(-x -v)
files=(iliad.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
# Multiple files
@test "Multiple files, one match, no flags" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="iliad.txt:Of Atreus, Agamemnon, King of men."
pattern="Agamemnon"
flags=()
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, several matches, no flags" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="midsummer-night.txt:Nor how it may concern my modesty,
midsummer-night.txt:But I beseech your grace that I may know
midsummer-night.txt:The worst that may befall me in this case,"
pattern="may"
flags=()
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, several matches, print line numbers flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="midsummer-night.txt:5:But I beseech your grace that I may know
midsummer-night.txt:6:The worst that may befall me in this case,
paradise-lost.txt:2:Of that Forbidden Tree, whose mortal tast
paradise-lost.txt:6:Sing Heav'nly Muse, that on the secret top"
pattern="that"
flags=(-n)
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, one match, print file names flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="iliad.txt
paradise-lost.txt"
pattern="who"
flags=(-l)
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, several matches, case-insensitive flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="iliad.txt:Caused to Achaia's host, sent many a soul
iliad.txt:Illustrious into Ades premature,
iliad.txt:And Heroes gave (so stood the will of Jove)
iliad.txt:To dogs and to all ravening fowls a prey,
midsummer-night.txt:I do entreat your grace to pardon me.
midsummer-night.txt:In such a presence here to plead my thoughts;
midsummer-night.txt:If I refuse to wed Demetrius.
paradise-lost.txt:Brought Death into the World, and all our woe,
paradise-lost.txt:Restore us, and regain the blissful Seat,
paradise-lost.txt:Sing Heav'nly Muse, that on the secret top"
pattern="TO"
flags=(-i)
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, several matches, inverted flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="iliad.txt:Achilles sing, O Goddess! Peleus' son;
iliad.txt:The noble Chief Achilles from the son
midsummer-night.txt:If I refuse to wed Demetrius."
pattern="a"
flags=(-v)
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, one match, match entire lines flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="midsummer-night.txt:But I beseech your grace that I may know"
pattern="But I beseech your grace that I may know"
flags=(-x)
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, one match, multiple flags" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="paradise-lost.txt:4:With loss of Eden, till one greater Man"
pattern="WITH LOSS OF EDEN, TILL ONE GREATER MAN"
flags=(-n -i -x)
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, no matches, various flags" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected=""
pattern="Frodo"
flags=(-n -l -i -x)
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, several matches, file flag takes precedence over line number flag" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="iliad.txt
paradise-lost.txt"
pattern="who"
flags=(-n -l)
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}
@test "Multiple files, several matches, inverted and match entire lines flags" {
[[ $BATS_RUN_SKIPPED == "true" ]] || skip
expected="iliad.txt:Achilles sing, O Goddess! Peleus' son;
iliad.txt:His wrath pernicious, who ten thousand woes
iliad.txt:Caused to Achaia's host, sent many a soul
iliad.txt:And Heroes gave (so stood the will of Jove)
iliad.txt:To dogs and to all ravening fowls a prey,
iliad.txt:When fierce dispute had separated once
iliad.txt:The noble Chief Achilles from the son
iliad.txt:Of Atreus, Agamemnon, King of men.
midsummer-night.txt:I do entreat your grace to pardon me.
midsummer-night.txt:I know not by what power I am made bold,
midsummer-night.txt:Nor how it may concern my modesty,
midsummer-night.txt:In such a presence here to plead my thoughts;
midsummer-night.txt:But I beseech your grace that I may know
midsummer-night.txt:The worst that may befall me in this case,
midsummer-night.txt:If I refuse to wed Demetrius.
paradise-lost.txt:Of Mans First Disobedience, and the Fruit
paradise-lost.txt:Of that Forbidden Tree, whose mortal tast
paradise-lost.txt:Brought Death into the World, and all our woe,
paradise-lost.txt:With loss of Eden, till one greater Man
paradise-lost.txt:Restore us, and regain the blissful Seat,
paradise-lost.txt:Sing Heav'nly Muse, that on the secret top
paradise-lost.txt:Of Oreb, or of Sinai, didst inspire
paradise-lost.txt:That Shepherd, who first taught the chosen Seed"
pattern="Illustrious into Ades premature,"
flags=(-x -v)
files=(iliad.txt midsummer-night.txt paradise-lost.txt)
run bash grep.sh "${flags[@]}" "$pattern" "${files[@]}"
assert_success
assert_output "$expected"
}

58
bash/grep/grep.sh Normal file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# parsing CLI options
while getopts :nlivx option; do
case "$option" in
# include line numbers in output
n) linenr=1 ;;
# only file names containing matching lines are printed
l) filename=1 ;;
# case insentivity
i) shopt -s nocasematch ;;
# matching lines are inverted (i.e., lines that do not match)
v) invert=1 ;;
# pattern should match whole line
x) entire=1 ;;
# other options are invalid and exit
\?) echo Invalid option.; exit 1;;
esac
done
# shifts the command-line arguments so that the remaining arguments
# are just the pattern and the files to search in
shift $((OPTIND-1))
pattern=$1
shift
files=( "$@" )
# if entire is set to 1, modifies the pattern to match the entire line
# by anchoring it with ^ (beginning of line) and $ (end of line)
(( entire )) && pattern="^$pattern$"
for file in "${files[@]}"; do
count=0
while read -r line
do
(( count++ ))
out=
if (( invert )); then
! [[ ${line} =~ ${pattern} ]] && out="$line"
else
[[ ${line} =~ ${pattern} ]] && out="$line"
fi
# if filename is set to 1, prints the file name and breaks out
# of the loop after the first matching line in each file
if (( filename )) && [[ -n $out ]]; then
echo $file;
break;
fi
if (( linenr )) && [[ -n $out ]]; then
out="$count":$out;
fi
if (( ${#files[@]} > 1 )) && [[ -n $out ]]; then
out="$file":$out;
fi
[[ -n $out ]] && echo $out;
done < "$file"
done
exit 0

BIN
exercism

Binary file not shown.