1

I'm new to shell scripting and are trying to learn the basics. I'm launching a script using the terminal such as "./scriptgoeshere.sh Argument1 Argument2".

I want the script to print some echo, "Hello World" is probably the best one to use, but I only want it to print out hello world if there are: Exactly two arguments, and those arguments have to consist solely of numbers. For example:

./scriptgoeshere.sh 1523 9643 ./scriptgoeshere.sh 7 96 Should work, however; ./scriptgoeshere.sh 594 arg Should NOT work, this is the script I have at the moment but it doesn't work, Ive tried figuring this out the entire day but cant seem to nail it, I even tried if/elif/else where I had it check if there are 2 inputs in the if part, and if they are both numbers in elif before continuing.

#!/bin/bash
if [ "$#" -eq 2 ] && [ "$@" -eq ^[0-9] ]; 
then
    i=0
    while [ $i -lt 3 ]
    do
        echo "Hello World!"
        sleep 5
        i=$((i+1))
    done
else
echo "Need two arguments and only numbers will be accepted, such as ./scriptgoeshere 153 251"

fi

It blocks the hello world loop until I put in 2 arguments, but it doesnt matter if they are numbers or text, and I get an error then: "./scriptgoeshere.sh: line 2: [: ^[0-9]: integer expression expected". I get that error even if I write ./scriptgoeshere.sh 123 123 in the terminal to launch it. I prefer to not solve it by variables as I want to figure out the arguments when launching the actual script.

This is doing my head in!

This is what I started with to make it check if there are exactly 2 arguments and it works, I just wanna add a small fix to it so those 2 arguments HAS to be numbers. I don't get why it seems to be so hard.

#!/bin/bash
if [ "$#" -eq 2 ];
then
    i=0
    while [ $i -lt 3 ]
    do
        echo "Hello World!"
        sleep 1
        i=$((i+1))
    done
else
echo "Need two argument, only numbers accepted"

fi

Edit: I solved it. I had worked so close.. I just needed to add extra [] to the number part of the if-string. Anyone knows why it needs to be written as [[ "$@" -eq ^[0-9] ]] while the first part can be written with just one [] ?

3
  • Sigh… If you keep changing your code I will never finish my answer that tries to elaborate your mistakes, reveal pitfalls and such. Oct 17, 2019 at 0:28
  • Haha I'm sorry! I've been working through a virtualbox and my keyboard inputs have been fucked and I haven't gotten around to fixing it yet, can't type []$@ for instance which means I've had it all copied over from the VMBox. Noticed a few things went missing and such, I updated it 23mins ago when I managed to solve it, did it previously when the code didnt copy-paste correctly. I managed to solve it with if [ "$#" -eq 2 ] && [[ "$@" =~ ^[0-9] ]];"
    – Trollblod
    Oct 17, 2019 at 0:41
  • And you even edited your comment after I read it! :/ And it says your solution is [[ "$@" =~ ^[0-9] ]] but the question body claims it is [[ "$@" -eq ^[0-9] ]]. Oct 17, 2019 at 1:40

1 Answer 1

0

At first [ "$#" -eq ^[0-9] ] or [ "$@" -eq ^[0-9] ] (the question was edited while I was composing my answer…). From the context I think you want to test if the parameter is a number.

  • $# is always a number, the number of positional parameters in decimal. You want to test two positional parameters: $1 and $2.
  • [ is a command ([ whatever ] is equivalent to test whatever). It may be a shell builtin but syntactically it's like ls or man. Words that follow (including ]) are parsed by the shell like any words that follow a command: shell expands them (e.g. $# becomes a number), removes quotes; eventually they become parameters to [. One of these expansions is filename expansion where various patterns are matched against existing filenames (filename expansion, globbing). Unquoted ^[0-9] may expand to ^2 ^5 if these files exist. If there is no match or if you disable the feature ^[0-9] will get to [ (as one of its arguments) literally. It looks like a pattern but [ doesn't support pattern matching anyway.
  • -eq requires the previous and the next argument to be integers. $# expands to an integer for sure. There is no way for ^[0-9] to expand to a number in this context.
  • Double-quoted $@ is special, it can expand to multiple words ("fields" in terms of POSIX) despite being quoted. Usually we quote to prevent word splitting: "$foo" expands to one word even if the value contains spaces and such. "$@" expands like "$1" "$2" …, its double-quotes prevent $1 to expand to multiple words (and separately $2 and so on). In this fragment:

    [ "$#" -eq 2 ] && [ "$@" -eq ^[0-9] ]
    

    the second [ is run iff the first succeeds (this is how && works), i.e. iff there are exactly two positional parameters. In such case "$@" is equivalent to "$1" "$2" and the second test becomes

    [ "$1" "$2" -eq ^[0-9] ]
    

    This doesn't make sense. [ may complain in different ways depending on actual values of parameters and expansion of ^[0-9], still it's garbage.


Now about [[ "$#" = [0-9] ]].

Note: this snippet was in the first revision of the question. The question was edited while I was composing my answer. I decided to keep this fragment because [[ may be useful eventually anyway.

  • Again: $# is always a number.
  • [[ is not equivalent to [. Any sane test you can do with [ you can rewrite with [[ (although you should't do this mindlessly by jut replacing [ with [[). There are additional tests [[ can do, it is more powerful. While [ (and test) is required by POSIX, [[ is not.

    Important thing: [[ is not a command like ls or [. It's a keyword, it changes the way the shell parses words that follow. Everything between [[ and ]] is parsed differently.

    If the operator is =, the next argument is a pattern. Some characters/syntaxes (e.g. * or [0-9]), when unquoted, are matched against the argument before =. It's like filename expansion, but not against existing filenames. Your code tested if (expanded) $# is exactly one digit. The whole test was

    [[ "$#" -eq 2 ]] && [[ "$#" = [0-9] ]]
    

    foo && bar runs bar iff foo succeeds. The second [[ was tested only if $# expanded to 2, so it had to succeed because 2 is exactly one digit. In this case the second [[ changes nothing.


Let's say you realized your mistake and tested if $1 is a number.

  • Like above [[ "$1" = [0-9] ]] returns success if $1 expands to exactly one digit.
  • [[ "$1" = [0-9][0-9] ]] – exactly two digits.
  • [[ "$1" = [0-9]* ]] – something that starts with a digit.

To detect any number of digits you need =~. This operator inside [[ ]] is somewhat similar to =, but now the pattern is an extended regular expression. It doesn't need to match the whole string to the left of the operator though, a substring is enough (therefore ^ and $ anchors are needed in your case):

  • [[ "$1" =~ [0-9] ]] returns success if $1 expands to a string containing a digit.
  • [[ "$1" =~ [0-9][0-9] ]] – … containing two digits next to each other.
  • [[ "$1" =~ ^[0-9]$ ]] – a string that is exactly one digit.
  • [[ "$1" =~ ^[0-9]*$ ]] – zero or more digits.
  • [[ "$1" =~ ^[0-9][0-9]*$ ]] – one or more digits.
  • [[ "$1" =~ ^[0-9]+$ ]] – one or more digits.

[[ "$1" =~ ^[0-9]+$ ]] and [[ "$2" =~ ^[0-9]+$ ]] are the tests you may want. To allow leading - modify them like this: [[ "$1" =~ ^-?[0-9]+$ ]]. The whole testing fragment may be

[ "$#" -eq 2 ] && `[[ "$1" =~ ^-?[0-9]+$ ]]` && `[[ "$2" =~ ^-?[0-9]+$ ]]`

Inside [[ ]] variables/parameters may not be double-quoted and their expanded values will not undergo word splitting and globbing. So you can write [[ $1 =~ ^-?[0-9]+$ ]] and it's safe. It's an exception; in general you should double-quote variables. Here double-quoting disturbs nothing, so I advise to quote anyway to get used to this good general practice.


You claim you solved the problem using [[ "$@" -eq ^[0-9] ]]. Sorry, I don't believe you. If there are exactly two positional parameters and they are numbers, I don't see a way this can be syntactically correct, let alone test what you want. In other cases… I still don't see a way.

… using [[ "$@" =~ ^[0-9] ]] (from your comment, incoherent with the question). This is not a proper solution. My tests indicate that "$@" inside [[ ]] behaves like unquoted $@ treated as one word without word splitting and globbing. This is very similar to how regular variables behave inside [[ ]], although it was not obvious to me in advance, because I know "$@" is not like regular variables.

Side note: for this reason I have never wanted to use "$@" inside [[ ]]. I cannot imagine a scenario when it's the right thing.

When there are exactly two positional parameters, your "solution" becomes [[ "$1 $2" =~ ^[0-9] ]]. It's syntactically valid and it checks if the very first character of the first argument to your script is a digit. Only this. Run ./scriptgoeshere.sh 0h well, the arguments will pass the test.


Notes:

  • [0-9] (as globbing pattern or regular expression) depends on your current locale. Most likely it will work as you expect, but technically it's possible to have a locale where 9 precedes 0 or where 2 is beyond 0-9. The most general way to match against a digit is [[:digit:]]. See this.

    On the other hand you may want to use $1 and/or $2 with shell arithmetic ($((…))), [ "$1" -ge 50 ] or such. Then you need Arabic numerals only. Even if [[:digit:]] includes them in your locale (it probably does), it may include other digits as well. Consider [0123456789]; it is explicit and not locale-dependent.

  • Not every string that matches ^-?[0-9]+$ is considered integer from the point of view of [ and/or [[ (e.g. when you do [ "$1" -eq 0 ]). Each tool has a range of integers it can handle. Similarly shell arithmetic may produce mathematically wrong results if you feed it with a string of digits that describes an out-of-range integer.

  • Mathematically 1.00 is an integer. Some tools may accept it as integer, some may not. What about 1e100?

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.