2

I am adding a script (command?) for a modified head command to my ~/.bashrc. It handles inputs as expected, both when manually entered, when derived from globbing, and when provided by stdin. However, if I forget to provide an argument to headj, my terminal hangs and can't be recovered with Ctrl-C.

Here is my code (with some calls to echo for debugging purposes:

headj(){
    echo "script makes it this far without arguments"
    IFS=" " read -r -a input <<< "${@:-$(</dev/stdin)}"
    echo "but but crashes before getting here"
    # 
    for i in ${input[@]}; do
        echo -e "███████████████████████████████████████████████████████████████████████████████"
        echo -e "headj $i start"
        head -50 "$i"
    done
}

And here is how it behaves:

$ headj list1
script makes it this far without arguments
but not this far
███████████████████████████████████████████████████████████████████████████████
headj list1 start
a
b

$ headj list*
script makes it this far without arguments
but not this far
███████████████████████████████████████████████████████████████████████████████
headj list1 start
a
b

███████████████████████████████████████████████████████████████████████████████
headj list2 start
c
d

$ ls -1 li* | headj
script makes it this far without arguments
but not this far
███████████████████████████████████████████████████████████████████████████████
headj list1 start
a
b

███████████████████████████████████████████████████████████████████████████████
headj list2 start
c
d

$ headj
script makes it this far without arguments

and that's where it hangs.

I've tried reading the inputs using input=( ${@:-$(</dev/stdin)} ) but get the same error. Adding this code at the beginning of the script handles the error.

if [ -z ${*:+x} ]; then
    echo "headj requires at least one argument"
    return
fi

Is there a better way to do this? Also, is there a keyword I should be searching for this type of problem?

12
  • not answer to your question but please read how to loop over positional parameters stackoverflow.com/q/192249
    – alecxs
    Jun 1, 2020 at 22:27
  • @alecxs, thanks. For more complex scripts I use while :; do; case $1 in; -a|--arg) OPTARG=$2 ... blah blah blah... ; shift;; esac; shift; done, but that seems overkills in this case, especially since all arguments to headj will be treated the same and headj doesn't take any optional arguments.
    – Josh
    Jun 1, 2020 at 22:45
  • ok i don't understand whats purpose of /dev/stdin. thought you could just use "$@" it contains all arguments passed to script (and you can check for $1 or even set default ${1:-default})
    – alecxs
    Jun 1, 2020 at 22:52
  • @alecxs, /dev/stdin is needed to accept information from pipes. Also, can you be a little more specific in your suggestion regarding "$0"? I just tried a number of different variations using "$0"` or $0; not all of them worked for my simple test and I'm pretty sure many would fail with edge cases.
    – Josh
    Jun 1, 2020 at 23:02
  • never used pipes in script, can't help with this. if we pass arguments ./script.sh ./dir1 "./dir 2" we could just use it straight for example find "$@" (will work with spaces in folder names). so ${@:-$(</dev/stdin)} will use positional parameters, or (as default) read from stdin? then this is probably the reason because you didn't pipe anything
    – alecxs
    Jun 1, 2020 at 23:10

2 Answers 2

2

Assuming you only run it without arguments or a pipe on mistake, and don't want to write filenames to the function's stdin (why would you), I'd add a check to see if stdin is a tty:

headj() {
    local files=()
    if [ "$#" -gt 0 ]; then
        files=("$@")
    elif ! tty >/dev/null; then
        readarray -t files
    else
        echo "Will not read filenames from a tty!" >&2
    fi
    for file in "${files[@]}"; do
        echo "do something with $file..."
    done
}

So, this works:

$ ls *.txt |headj
do something with test.txt...

But this complains:

$ headj
Will not read filenames from a tty!

Note that the read command you used would split filenames with whitespace, so I'd avoid doing that, hence the condition on the number of arguments and the array above. readarray should read a list of newline separated items, which is what ls and find produce (when piped), but of course it means filenames with newlines embedded wouldn't work.

(Nitpicky perhaps, but I also wouldn't use "$@" in the argument to <<< or in an assignment, since it's supposed to produce multiple words, which it can't do in those cases. Using <<< "$*" would make it clearer that the intent is to combine the arguments to a single string.)

As for why ^C wouldn't work, I can't repeat that, it doesn't look like the function should do that.

1
  • That works perfectly! Thanks.
    – Josh
    Jun 4, 2020 at 12:50
0

I just learned how to add a function into bashrc. Thanks for that. Try this for your problem.

headj(){
  ARG=${1? Error. Please, provide the input file. Example: headj filename}
  #echo "script makes it this far without arguments"
  IFS=" " read -r -a input <<< "${@:-$(</dev/stdin)}"
  #echo "but but crashes before getting here"
  # 
  for i in ${input[@]}; do
    echo -e ""`enter code here`
    echo -e "headj $i start"
    head -50 "$i"
done}
$headj

bash: 1: Error. Please, provide the input file. Example: headj filename

Example with pipe

$echo "It works for me." > answer.txt
$ cat answer.txt

It works for me.

$headj  answer.txt  | awk '{print $_}'

headj answer.txt start

It works for me.

headj  answer.txt  | awk '{print $_}' | sed 's/me./me. It should work for you too./g'

headj answer.txt start

It works for me. It should work for you too.

5
  • Linuxes, did you test your code with any of the pipe examples in my question? It doesn't work.
    – Josh
    Jun 2, 2020 at 1:46
  • @Josh, i tested it with pip and works fine.
    – zesys
    Jun 2, 2020 at 5:39
  • @Josh, I noticed you are trying to use head on a variable ( not a file). While there may be a way to get it work I should point out head is designed to work with files. How about you temporarily save the output of any command to a file and use your code on it. Here is an example: $ ls -1 *.txt>list | headj list && rm list
    – zesys
    Jun 2, 2020 at 6:26
  • you did not pipe anything into headj
    – alecxs
    Jun 2, 2020 at 6:26
  • @alecxs, yes I just noticed that. Thanks. See my comment above.
    – zesys
    Jun 2, 2020 at 6:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.