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.
    – Kusalananda
    CommentedMar 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.CommentedMar 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.CommentedMar 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 resultsCommentedMar 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 fileresult-of-selection. More funny results with the-script -f -f.CommentedMar 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.CommentedMar 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
        – ilkkachu
        CommentedMar 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.
        – CommunityBot
        CommentedMar 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.