1

So I've ran into a bit of a wall, I have an option in my script that calls a function which allows me to specify a file/directory and then I want to parse that output into a menu tool (using dmenu in this case) to select which file is the one I want specifically and continue working with that selection as a variable in the same script. This works fine if it's just one file or directory, but I want to be able to use the option multiple times and then parse all of that output at once to dmenu. Here's a snippet

fileSelection () {
    if [ -d "${OPTARG}" ]; then find "${OPTARG}" -type f; fi;
    if [ -f "${OPTARG}" ]; then printf '%s\n' "${OPTARG}"; fi;
}

while getopts "f:" option; do
   case "${option}" in
       f) file="$(fileSelection|dmenu)";;
   esac
done

And like I said this works if I do:

myscript -f file

or

myscript -f directory

but I was hoping to also be able to do this:

myscript -f file1 -f file2

The problem is, since the function is called consecutively I can't parse the output into dmenu like that, because it doesn't invoke dmenu with options file1 and file2, but first with file1 and then with file2, I hope this makes sense.

There might be some really simple solution I am missing, I've thought about simply writing the output into a file and then parsing that which might work, but I'd like to avoid piping to files if possible. I am also trying to keep it POSIX compliant, and would appreciate answers that follow that.

2
  • I have rolled back your recent edit, which tagged the question title with "SOLVED", and added an answer to the question text. If you have found an answer other than the ones posted already, please use the "Your Answer" box at the bottom of the page to post your additional answer. After some delay, you may accept your answer (or any other answer) by ticking the greyed-out checkmark to the left of the answer. Accepting an answer in this way marks the issue as having been resolved. Commented Mar 19 at 21:54
  • Right, my bad, I am new to this platform and still getting acquainted with how things work. I appreciate the help. Commented Mar 20 at 19:24

4 Answers 4

1

You can collect the values derived from your -f and pass them all together to dmenu. Here's an example that will work for most directories and files - but not those containing an embedded newline:

#!/bin/bash
#
if ! type dmenu >/dev/null 2>&1
then
    # Demonstration code in case dmenu is not installed
    dmenu() {
        echo "This is a fake implementation of dmenu" >&2
        sed 's/^/>   /' >&2
    }
fi


# List of files from the -f option
files=()

# Loop across the command line
while getopts "f:" OPT
do
   case $OPT in
       f)
            # Disable globbing
            OIFS="$IFS" IFS=$'\n'
            noglob=$(set -o | awk '$1=="noglob"{print $2}')
            set -o noglob

            # Capture the specified set of files
            files+=( $(find "$OPTARG" -type f) )

            # Reenable globbing
            IFS="$OIFS"
            [ "$noglob" = 'off' ] && set +o noglob
            ;;
   esac
done

# If we have some files then pass them to dmenu
[ "${#files[@]}" -gt 0 ] && printf '%s\n' "${files[@]}" | dmenu

Since you want a POSIX solution, and you're already assuming file names cannot contain newlines, you could build up a single string of newline-terminated values and use that. Actually, in this case I'd probably build the set of starting points and then use find | dmenu across them rather than build the set of results like I have in my first approach.

#!/bin/sh

# List of source files from the -f option
files=

# Loop across the command line
nl='
'
while getopts "f:" OPT
do
   case $OPT in
       f)
            # Capture the source file name
            files="$files$OPTARG$nl"
            ;;
   esac
done

# If we have some files then pass them to dmenu
if [ -n "$files" ]
then
    while [ -n "$files" ]
    do
        file=${files%%$nl*}     # get first NL-terminated entry
        files=${files#*$nl}     # strip it off the list
        find "$file" -type f    # generate the set of files
    done |
        dmenu
fi
0
0

Your fileSelection|dmenu assumes file paths don't contain newline characters anyway, so you can make $file a newline separated/delimited list:

NL='
'
files=
...
  (f) files=$files$(fileSelection|dmenu)$NL;;
...

# to use $files, like pass each file as separate arguments to a
# command, use split+glob with glob disabled

IFS=$NL; set -o noglob
cmd -- $files

An alternative is to generate the set -- ... shell code to set the list of positional parameters to the list of files

sh_quote() {
  LC_ALL=C sed "s/'/'\\\\''/g; 1s/^/'/; \$s/\$/'/"
}

files=
...
  (f) files="$files $(fileSelection|dmenu|sh_quote)";;
...

# and to use either
eval "cmd -- $files"

# or
eval "set -- $files"
cmd -- "$@"

POSIXly, that's still dangerous as sed is not required to work correctly with lines exceeding LINE_MAX bytes in length and the output of find is not guaranteed to have lines as short (not even shorter than PATH_MAX itself not guaranteed to be shorted than LINE_MAX).

1
  • Thanks a lot, I'll make try to implement something of this sorts. Once I'm happy with how it's turned out, later today I hope, I'll post the whole snippet here. Commented Mar 19 at 10:52
0

Make the option-parsing while-loop output the found names, and pass the output of it to dmenu. I.e., move the piping to dmenu from the output from fileSelection to the output of the while-loop.

To do this correctly, we first need to be certain that there actually are at least one -f option on the command line. This means having to parse the options twice: once to handle any other option and to detect a -f, and once more to act on the -f options and to get the file list and let the user pick filename into our file variable.

#!/bin/sh

usage () {
        echo 'use with "-h" or "-f somepath" (optionally repeated)'
}

fileSelection () {
    if [ -d "${OPTARG}" ]; then find "${OPTARG}" -type f; fi;
    if [ -f "${OPTARG}" ]; then printf '%s\n' "${OPTARG}"; fi;
}

# Parse command line options and set a flag
# if we have any -f options to deal with.
f_flag=false
while getopts hf: opt; do
        case $opt in
                h) usage; exit ;;
                f) f_flag=true ;;
                *) echo 'error' >&2; exit 1
        esac
done

if "$f_flag"; then
        # Now parse all options again,
        # but only care about the -f options.
        OPTIND=1

        file=$(
                while getopts hf: opt; do
                        case $opt in (f) fileSelection; esac
                done | dmenu
        )
fi
-1

I have maybe found a solution after looking more into the POSIX way of doing arrays

#!/bin/sh

fileSelection () {
    printf '%s\n' "${OPTARG}"
}

while getopts "f:" option; do
case "${option}" in
    f) set -- "$@" "$(fileSelection)";;
esac
done

printf '%s\n' "$@" | sed "/^-/ d;"

this is a very simplified version of the script but simply parsing from sed into dmenu should produce what I want. I'll double check once I am home. Thanks to Chris Davies for pointing me in the right direction.

5
  • 1
    That's not POSIX: Any other attempt to invoke getopts multiple times in a single shell execution environment with parameters (positional parameters or param operands) that are not the same in all invocations, or with an OPTIND value modified by the application to be a value other than 1, produces unspecified results Commented Mar 19 at 9:12
  • With the-script invoked as the-script -f file, you end up with -f file result-of-selection which sed (btw same as grep -v '^-') will strip to file result-of-selection. More funny results with the-script -f -f. Commented Mar 19 at 9:15
  • Thanks, I didn't know about the getopts thing, I'll have to make sure to look into that for later. Commented Mar 19 at 10:47
  • @hollowillow, for a concrete example, try something like sh -c 'while getopts ab: opt; do echo "$opt $OPTARG"; set -- x -b xyz ; done' sh -a -b foo Commented Mar 20 at 7:49
  • As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center. Commented Mar 27 at 5:11

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.