3

I have a file that documents the structure of several directories. I am trying to print the text for each directory individually. My input file looks like this:

$ cat file.txt
/bin:
file_1
file_2
file_3

/sbin:
file_a
file_b
file_c

/usr/local/bin:
doc_a
doc_b
doc_c

What I'm trying to do is print a specific section of the file based on user selection:

#!/bin/bash

PS3=$'\nMake a selection '
select dir in $(grep ':' file.txt;) do
    case $REPLY in
        [0-9]) echo $dir
               # Need something here. Maybe a pcregrep regex?
               # pcregrep '(<= $dir)*(some_fancy_regex)' file.txt
               break;;
    esac
done

The user is presented with menu options:

1) /bin:
2) /sbin:
3) /usr/local/bin:

Make a selection

Suppose the user chooses 2. Currently, this just prints the chosen directory on the screen. I would like to display the directory as well as the files it contains.

/sbin:
file_a
file_b
file_c

From what I've read it seems like a pcre regex would work here. I barely understand non-pcre style regex. I'm trying to wrap my brain around positive and negative lookahead & lookbehind but I really don't know what I'm doing yet. If someone could help me figure this out I would appreciate it.

  1. All directories begin with a / and end with :
  2. File names listed under each directory may contain:
    • [a-z], [A-Z],[0-9]
    • Literal characters . _ - [
  3. All directory / file structures end with a blank empty line
I0_ol
  • 1,054
  • 1
  • 14
  • 28
  • A lot of really great answers on this question. It's difficult to say which one 'solved' the problem because at this point, all of the answers have done that. – I0_ol Mar 19 '17 at 07:26
  • Only [one of them](http://stackoverflow.com/a/42875201/1745001) does not involve a shell loop so [why-is-using-a-shell-loop-to-process-text-considered-bad-practice](http://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-considered-bad-practice) might help. Also that same one is the only one that will work for all contents of your input file and is portable across all bourne-derived shells on all UNIX systems. The others will only work in a specific shell and/or will fail when your input contains regexp metacharacters or escaped characters or.... – Ed Morton Mar 19 '17 at 13:22

4 Answers4

1

With GNU sed and bash:

dir="/usr/local/bin:"
sed -n "/${dir//\//\/}/,/^$/{/^$/d;p}" file

With bash:

dir="/usr/local/bin:"
while IFS= read -r line; do
  [[ $line == $dir ]] && switch=1
  [[ $line == "" ]] && switch=0
  [[ $switch == 1 ]] && echo "$line"
done < file

Output in both cases:

/usr/local/bin:
doc_a
doc_b
doc_c
Cyrus
  • 84,225
  • 14
  • 89
  • 153
1

Grep is not to best tool to do this because it is line-oriented; you can't really have grep look at expressions that span multiple lines, except with some contortion – and that -z option is not specified by POSIX.

You could do it like this:

#!/bin/bash

PS3=$'\nMake a selection '

mapfile -t opts < <(grep ':' file.txt)

select dir in "${opts[@]}"; do
    sed -n "\@$dir@,/^$/{/^$/q;p}" file.txt
    break
done

First, I've changed your menu creation. Notice that you have a spare semicolon within the command substitution and a missing one after it; using grep like this would also break if there are spaces in the directory names. I've thus used mapfile to get all the lines containing : into an array.

Then, once I know about the directory, I use sed to print "from the directory name on until the next empty line". That would simply be

sed -n "/$dir/,/^$/p"

but this falls short on multiple fronts. First of all, the directory name can contain slashes, which trips up the / delimited addressing. We can use \%regexp% instead of /regexp/, where % can be any character; I've chosen @.

Now, we have

sed -n "\@$dir@,/^$/p"

That's almost there, but prints trailing blank lines; we suppress that by using {/^$/q;p} instead of just p, which says "if the line is blank, quit, else print it".

Sample output (edited to use a directory name with a space):

1) /bin blah:
2) /sbin:
3) /usr/local/bin:

Make a selection 1
/bin blah:
file_1
file_2
file_3

Remark: Non-GNU seds (like the one found in macOS) might complain about the two commands in curly braces; using {/^$/q;p;} instead (extra semicolon) might help.

Community
  • 1
  • 1
Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
1

It can be done purely in bash 4 in a single pass without using any external tool. Here is the script to solve this problem:

#!/bin/bash

# declare an associative array
declare -A dirs=()

# loop thru all lines and populate our associate array
# with dir as key and \n separated file names as value
while read -r; do
   [[ -z $REPLY ]] && continue
   if [[ $REPLY == *: ]]; then
      d="$REPLY"
   else
      dirs["$d"]+=$'\n'"$REPLY"
   fi
done < file.txt

# present a menu to customer and print selected dir name with file names
select dir in "${!dirs[@]}"; do
   if [[ -n $dir ]]; then
      printf '%s%s\n' "$dir" "${dirs[$dir]}"
      break
   fi
done

Output:

1) /usr/local/bin:
2) /bin:
3) /sbin:
#? 1
/usr/local/bin:
doc_a
doc_b
doc_c

and this:

1) /usr/local/bin:
2) /bin:
3) /sbin:
#? 3
/sbin:
file_a
file_b
file_c
anubhava
  • 761,203
  • 64
  • 569
  • 643
  • Added a sanity check in `select` loop to make sure a valid menu item is selected. – anubhava Mar 19 '17 at 07:54
  • `select` will put empty value in `dir` variable when an invalid choice is typed by user. If we don't check using above condition then break will stop the select loop. – anubhava Mar 20 '17 at 03:04
  • I think I basically understand most of this. But could you explain the line `dirs["$d"]+=$'\n'"$REPLY"`? – I0_ol Mar 20 '17 at 05:36
  • `dirs["$d"]+=$'\n'"$REPLY"` is appending a **newline + current line** into array entry with the key as `$d` (we set `d` to a line that ends with colon earlier) We use `$'\n'` notation to prefix each entry with newline. – anubhava Mar 20 '17 at 05:58
1

Don't mistake shell for a text-processing tool, that's what awk is for. All you need are these 4 lines:

$ cat tst.sh
awk -v RS= -F'\n' -v OFS=') ' '{print NR, $1}' file.txt >&2
printf '\nMake a selection: ' >&2
IFS= read -r rsp
awk -v RS= -v nr="$rsp" 'NR==nr' file.txt

$ ./tst.sh
1) /bin:
2) /sbin:
3) /usr/local/bin:

Make a selection: 2
/sbin:
file_a
file_b
file_c
Ed Morton
  • 188,023
  • 17
  • 78
  • 185