1

I'm trying to write a script that takes a port number as an argument. It returns the next port that is not assigned to anything, and checks it by using the file /etc/services. If the port is taken (i.e. listed in the document), it adds one and then tries again. I can't seem to get this script to return anything for "found" - it always equals 0 so I never enter the while loop. What am I doing wrong?

#!/bin/bash

port=$1
found=$(cat /etc/services | grep -o '[[:space:]]$port/' | wc -l)

while [ $found -ge 1 ]; do
    $port=$($port+1)
#done

echo "found: $found"
echo "port: $port"

(ignore the comment in front of done, that's not the problem)

Jeff Schaller
  • 66,199
  • 35
  • 114
  • 250
  • You're not reinitializing found inside the loop, and you can't assign $found; you want found= ... – Jeff Schaller May 04 '17 at 00:22
  • 2
    Plug for shellcheck.net for the syntactic error checking – Jeff Schaller May 04 '17 at 00:24
  • I have transcribed your image to text. For a bit of rationale behind this, see [this Meta post](https://unix.meta.stackexchange.com/q/4086) – Fox May 04 '17 at 01:49
  • 1
    Related: https://unix.stackexchange.com/questions/180492/is-it-possible-to-connect-to-tcp-port-0/180500#180500 – Kusalananda May 04 '17 at 06:38
  • I wonder if you meant for the `grep` check to be done within the loop, now you'd just return the next number even if that was reserved too. – ilkkachu May 04 '17 at 16:58

2 Answers2

2

Overview

There are just a few minor errors with this script, and a few stylistic things I would change. Let's go through the original line-by-line:

#!/bin/bash

port=$1

A typical #! line and a simple assignment, we're off to a good start.

found=$(cat /etc/services | grep -o '[[:space:]]$port/' | wc -l)

This line has a small issue that one may not notice at first glance. Since $port is in strong quotes, '…$port', it won't expand to its value. You'll want to use weak quotes instead, "…$port". Also, you don't really need cat here. grep takes a filename as an argument, but if it didn't you could always use redirection (grep 'pattern' < /etc/services). Further, grep -c writes a count of matching lines, so wc -l is redundant here as well.

while [ $found -ge 1 ]; do
    $port=$($port+1)
done

The loop seems almost fine. You'll want to quote the entities in the test in case $found could possibly be empty. Also, you assign with port=, not $port= (but you've used the correct form above, so I assume this was just a typo). Finally $($port+1) means (if port is 22, for example) "The output of the command 22+1". Clearly you want "The result of the arithmetic expression 22+1", which is the very similar $(($port+1)).

echo "found: $found"
echo "port: $port"

These lines are alright as is.

Considering the logic

Now, if you make all of the above changes so that the program is syntactically correct, it still will not do what you want. You don't list a "correct" output, so I'm assuming you want:

found: <the number of occurrences of $1 in /etc/services>
port: <the next free port>

If these assumptions are incorrect, let me know in the comments and I will edit accordingly.

The script as is will never return if the given port does appear in /etc/services because you never update found. So you may think it would be wise to copy the found=… line into the loop, but that is not so! If you did that, then the script would always return found: 0. I think the best thing to do here is to create a function:

found () {
    grep -co "[[:space:]]$1/" /etc/services
}

but now the exit value of this function can directly work as a condition. You'll want to quiet the output so it doesn't appear along with yours, and then you'll no longer need the -co flags, but you will want -q:

while found "$port"; do

Then, to return the number of occurrences of the input port, it would be

echo "found: $(found "$1")"

or you could use printf.

Putting it all together

I think this is the script you were trying to write:

#!/bin/sh

found () {
    grep -q "[[:space:]]$1/" /etc/services
}

port=$1

while found "$port"; do
    port=$(($port+1))
done

printf 'found: %d\nport %d\n' "$(found "$1")" "$port"

Bonus

As I was writing this, my initial reaction was that counting lines matching a given pattern could be done in awk as well as grep. It then occurred to me that the entire program could be written in awk. As a bonus, here is one implementation of that (though here I assume that /etc/services is sorted by port number):

#!/usr/bin/awk -f

BEGIN {
    FS = "[ \t/][ \t/]*"
    ARGC = 2
    PORT = ARGV[1]
    OPORT = PORT
    ARGV[1] = "/etc/services"
}
($2 == OPORT) {
    FOUND = FOUND + 1
}
(NEXT == 0 && NR > 1 && $2 > PORT) {
    NEXT = ($2 > PORT + 1) ? PORT + 1 : NEXT
    PORT = $2
}
END {
    printf "found: %d\nport: %d\n", FOUND, (FOUND == 0) ? OPORT : NEXT
}
Fox
  • 8,013
  • 1
  • 26
  • 44
  • I got that much. On my system, if you run this with a port number of 22, it will say `found: 3, port: 24`, as in "There were 3 entries for port 22 in /etc/services, and the next available port is 24". Are you looking for something different? – Fox May 04 '17 at 03:20
  • @Kay I originally posted an incorrect `awk` version. Then I removed it, but decided to fix it and put it back in. You might try both the `bash` and `awk` versions. You'll find that the `awk` version is noticeably faster, because it does not need to search through the entire file for each increment of `PORT` – Fox May 04 '17 at 03:37
  • I have to object to `'[[:space:]]'"$1"'/'` on aesthetic grounds. The `[]/` aren't special in double quotes, so you could just do `"[[:space:]]$1/"` and avoid the multi-quote dance... – ilkkachu May 04 '17 at 17:00
  • @ilkkachu After thinking for a while, I agree with you. Edited to universal weak quotes. I was already in violation of my own preference for strong quotes whenever possible with `"1"` – Fox May 05 '17 at 00:05
  • This would beea lot more idiomatic if the function's exit code was 0 for found, non-zero otherwise. Capturing the output number and comparing it lexically to another is clumsy, inelegant, and error-prone, and a common antipattern. – tripleee May 05 '17 at 14:03
-1

Minus the math part, here's a quick and dirty POC to accomplish what you're going for, but using an "if" statement in lieu of "while" loop conditions for your logic.

#!/bin/bash

port=$1
echo $port | while read a; do
found=$(cat /etc/services | grep -Eo '[0-9]{1,5}/' | sed 's/^/ /' | grep ' '$a'/')
if [[ $found ]];
then echo port $a is being used
else echo port $a is not being used
fi
done

Edit: The issue was stemming from the $found variable. the single quotes in the grep make $port get interpreted literally, so its grepping for the string ' $port/' instead of ' 80/' for example. fix: just change the single quotes to double quotes.

found=$(cat /etc/services | grep -o "[[:space:]]$port/" | wc -l)

Rui F Ribeiro
  • 55,929
  • 26
  • 146
  • 227
  • I haven't read this thoroughly, but "grepping for a space in front of the port numbers" is not what `[[:space:]]` means. That is a POSIX character class that matches _any_ whitespace character, including tab, formfeed, or U2008 (thin space) – Fox May 04 '17 at 16:19
  • @Fox you are right about the [[:space:]], I did not know that. In that case all you need to do is change your single quotes to double quotes to allow string interpolation. The single quotes interpret '$port' as a literal instead of substituting the value, which should be a number in this case. `found=$(cat /etc/services | grep -o "[[:space:]]$port/" | wc -l)` – TheWhiteCong May 04 '17 at 16:41