diff --git a/bash/grep/.exercism/config.json b/bash/grep/.exercism/config.json new file mode 100644 index 0000000..042dd24 --- /dev/null +++ b/bash/grep/.exercism/config.json @@ -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" +} diff --git a/bash/grep/.exercism/metadata.json b/bash/grep/.exercism/metadata.json new file mode 100644 index 0000000..a697aa6 --- /dev/null +++ b/bash/grep/.exercism/metadata.json @@ -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} \ No newline at end of file diff --git a/bash/grep/HELP.md b/bash/grep/HELP.md new file mode 100644 index 0000000..40ffaed --- /dev/null +++ b/bash/grep/HELP.md @@ -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. \ No newline at end of file diff --git a/bash/grep/README.md b/bash/grep/README.md new file mode 100644 index 0000000..490d9f8 --- /dev/null +++ b/bash/grep/README.md @@ -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 \ No newline at end of file diff --git a/bash/grep/bats-extra.bash b/bash/grep/bats-extra.bash new file mode 100644 index 0000000..54d4807 --- /dev/null +++ b/bash/grep/bats-extra.bash @@ -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 +# +# 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 +# . +# + +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 +} diff --git a/bash/grep/grep.bats b/bash/grep/grep.bats new file mode 100644 index 0000000..03937dc --- /dev/null +++ b/bash/grep/grep.bats @@ -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" +} diff --git a/bash/grep/grep.sh b/bash/grep/grep.sh new file mode 100644 index 0000000..ebdf063 --- /dev/null +++ b/bash/grep/grep.sh @@ -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 diff --git a/exercism b/exercism index 0b52396..4379360 100755 Binary files a/exercism and b/exercism differ