1114

I would like to check if a string begins with "node" e.g. "node001". Something like

if [ $HOST == node* ]
  then
  echo yes
fi

How can I do it correctly?


I further need to combine expressions to check if HOST is either "user1" or begins with "node":

if [ [[ $HOST == user1 ]] -o [[ $HOST == node* ]] ];
then
echo yes
fi

> > > -bash: [: too many arguments

How can I do it correctly?

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
Tim
  • 1
  • 141
  • 372
  • 590
  • 7
    Don't be too tempted to combine expressions. It may look uglier to have two separate conditionals, though you can give better error messages and make your script easier to debug. Also I would avoid the bash features. The switch is the way to go. – hendry Jan 31 '10 at 17:02

13 Answers13

1512

This snippet on the Advanced Bash Scripting Guide says:

# The == comparison operator behaves differently within a double-brackets
# test than within single brackets.

[[ $a == z* ]]   # True if $a starts with a "z" (wildcard matching).
[[ $a == "z*" ]] # True if $a is equal to z* (literal matching).

So you had it nearly correct; you needed double brackets, not single brackets.


With regards to your second question, you can write it this way:

HOST=user1
if  [[ $HOST == user1 ]] || [[ $HOST == node* ]] ;
then
    echo yes1
fi

HOST=node001
if [[ $HOST == user1 ]] || [[ $HOST == node* ]] ;
then
    echo yes2
fi

Which will echo

yes1
yes2

Bash's if syntax is hard to get used to (IMO).

Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128
Mark Rushakoff
  • 249,864
  • 45
  • 407
  • 398
  • 22
    For regex do you mean [[ $a =~ ^z.* ]] ? – JStrahl Mar 18 '13 at 07:13
  • 4
    So is there a functional difference between `[[ $a == z* ]]` and `[[ $a == "z*" ]]`? In other words: do they work differently? And what specifically do you mean when you say "$a is equal to z*"? – Niels Bom Jun 16 '15 at 10:37
  • 9
    You don't need the statement separator ";" if you put "then" on its own line – Yaza Oct 14 '15 at 11:45
  • @NielsBom: the first one will match any string starting with `z`, the second will only match a literal `z*`. – naught101 Jul 21 '16 at 06:30
  • 14
    Just for completeness: To check if a string ENDS with ... : `[[ $a == *com ]]` – lepe Feb 06 '17 at 02:56
  • 2
    This is not useful without a discussion about how to deal with shell expansion of the wildcard. – R.M. May 19 '17 at 18:34
  • 7
    The ABS is an unfortunate choice of references -- it's very much the W3Schools of bash, full of outdated content and bad-practice examples; the freenode #bash channel has been trying to discourage its use [at least since 2008](http://wooledge.org/~greybot/meta/abs). Any chance of repointing at [BashFAQ #31](http://mywiki.wooledge.org/BashFAQ/031)? (I'd also have suggested the Bash-Hackers' wiki, but it's been down for a bit now). – Charles Duffy Oct 21 '17 at 23:19
  • 2
    Does this not need quotes around the lhs (eg. `[[ "$a" == z* ]]`)? If not, why? Wouldn't an empty `a` cause problems? – Bailey Parker Sep 01 '18 at 14:59
  • 2
    No word splitting or glob expansion will be done for `[[`, so you don't need to quote `a$`. [Reference](http://mywiki.wooledge.org/BashFAQ/031). – Christian Long Sep 25 '18 at 14:51
  • 1
    Please note that double brackets are bash specific and not POSIX compliant. They won't work the same way in other shells. – Bachsau Oct 21 '18 at 14:47
  • 1
    Is there any functional difference between `[[ $HOST == user1 ]] || [[ $HOST == node* ]]` and `[[ $HOST == user1 || $HOST == node* ]]`? – Adam Feb 27 '19 at 20:34
  • If your pattern contains a space, replace the space with `[[:space:]]`, e.g., `if [[ "${a}" != test[[:space:]]host* ]]` – Searene Jul 30 '19 at 11:36
  • 1
    In case you need to have whitespaces, you may wrap the value with single quotes `if [[ $3 == 'list naptrrecord'* ]] then echo "" fi` – anandbibek Dec 04 '19 at 06:17
  • 1
    why is it so easy, _really bash, i'm disappointed that i will be able to remember this and use it without checking the docs_. – WEBjuju Sep 29 '20 at 15:49
  • A simple shell (not bash) test like `[ -z "${HOST##user*}" ]` would be enough... why all this complicated bash things? – Sandburg Oct 10 '20 at 10:52
  • What about [], single bracket? [ $a == str... ] – Bob Jul 05 '21 at 04:41
  • rationale: wildcards will only be in effect if the value is given without quotes. – Alexander Stohr Dec 08 '21 at 14:27
  • man, bash is such a train wreck, hard to wrap you're head around some of the more ridiculous aspects of it. trying to make this work when I want to test if a string starts with a `#`, no luck so far – DanDan Mar 09 '23 at 10:27
  • Why `[[ $a == "#*" ]]` does not work? How to fix? Note: the `if [[ $a == \#* ]]` is working. – pmor May 18 '23 at 10:42
  • Ah, sorry! I've overlooked: `[[ $a == "z*" ]]` is true if `$a` is _equal_ to `z*`. – pmor May 26 '23 at 11:57
358

If you're using a recent version of Bash (v3+), I suggest the Bash regex comparison operator =~, for example,

if [[ "$HOST" =~ ^user.* ]]; then
    echo "yes"
fi

To match this or that in a regex, use |, for example,

if [[ "$HOST" =~ ^user.*|^host1 ]]; then
    echo "yes"
fi

Note - this is 'proper' regular expression syntax.

  • user* means use and zero-or-more occurrences of r, so use and userrrr will match.
  • user.* means user and zero-or-more occurrences of any character, so user1, userX will match.
  • ^user.* means match the pattern user.* at the begin of $HOST.

If you're not familiar with regular expression syntax, try referring to this resource.

Note that the Bash =~ operator only does regular expression matching when the right hand side is UNQUOTED. If you do quote the right hand side, "any part of the pattern may be quoted to force it to be matched as a string.". You should not quote the right hand side even when doing parameter expansion.

Flimm
  • 136,138
  • 45
  • 251
  • 267
brabster
  • 42,504
  • 27
  • 146
  • 186
  • Thanks, Brabster! I added to the original post a new question about how to combine expressions in if cluase. – Tim Jan 31 '10 at 16:44
  • 3
    Its a pity that the accepted answer does not says anything about the syntax of regular expression. – CarlosRos Apr 26 '17 at 18:08
  • 66
    **FYI the Bash `=~` operator only does regular expression matching when the right hand side is UNQUOTED. If you do quote the right hand side "Any part of the pattern may be quoted to force it to be matched as a string."** (1.) make sure to always put the regular expressions on the right un-quoted and (2.) if you store your regular expression in a variable, make sure to NOT quote the right hand side when you do parameter expansion. – Trevor Boyd Smith Feb 15 '18 at 16:34
258

I always try to stick with POSIX sh instead of using Bash extensions, since one of the major points of scripting is portability (besides connecting programs, not replacing them).

In sh, there is an easy way to check for an "is-prefix" condition.

case $HOST in node*)
    # Your code here
esac

Given how old, arcane and crufty sh is (and Bash is not the cure: It's more complicated, less consistent and less portable), I'd like to point out a very nice functional aspect: While some syntax elements like case are built-in, the resulting constructs are no different than any other job. They can be composed in the same way:

if case $HOST in node*) true;; *) false;; esac; then
    # Your code here
fi

Or even shorter

if case $HOST in node*) ;; *) false;; esac; then
    # Your code here
fi

Or even shorter (just to present ! as a language element -- but this is bad style now)

if ! case $HOST in node*) false;; esac; then
    # Your code here
fi

If you like being explicit, build your own language element:

beginswith() { case $2 in "$1"*) true;; *) false;; esac; }

Isn't this actually quite nice?

if beginswith node "$HOST"; then
    # Your code here
fi

And since sh is basically only jobs and string-lists (and internally processes, out of which jobs are composed), we can now even do some light functional programming:

beginswith() { case $2 in "$1"*) true;; *) false;; esac; }
checkresult() { if [ $? = 0 ]; then echo TRUE; else echo FALSE; fi; }

all() {
    test=$1; shift
    for i in "$@"; do
        $test "$i" || return
    done
}

all "beginswith x" x xy xyz ; checkresult  # Prints TRUE
all "beginswith x" x xy abc ; checkresult  # Prints FALSE

This is elegant. Not that I'd advocate using sh for anything serious -- it breaks all too quickly on real world requirements (no lambdas, so we must use strings. But nesting function calls with strings is not possible, pipes are not possible, etc.)

Jo So
  • 25,005
  • 6
  • 42
  • 59
  • 26
    +1 Not only is this portable, it's also readable, idiomatic, and elegant (for shell script). It also extends naturally to multiple patterns; `case $HOST in user01 | node* ) ...` – tripleee Sep 06 '13 at 07:12
  • Is there a name for this type of code formatting? `if case $HOST in node*) true;; *) false;; esac; then` I've seen it here and there, to my eye it looks kinda scrunched up. – Niels Bom Jun 16 '15 at 10:28
  • @NielsBom I don't know what exactly you mean by formatting, but my point was that shell code is very much *composable*. Becaues `case` commands are commands, they can go inside `if ... then`. – Jo So Jun 16 '15 at 16:58
  • I don't even see why it's composable, I don't understand enough shell script for that :-) My question is about how this code uses non-matched parentheses and double semicolons. It doesn't look anything like the shell script I've seen before, but I may be used to seeing bash script more than sh script so that might be it. – Niels Bom Jun 17 '15 at 13:01
  • Note: It should be `beginswith() { case "$2" in "$1"*) true;; *) false;; esac; }` otherwise if `$1` has a literal `*` or `?` it might give wrong answer. – LLFourn Feb 05 '16 at 11:50
  • @LLFourn: Thanks! You are right. I corrected it. But you don't need quotes around `$2`. – Jo So Feb 05 '16 at 17:33
  • While the shell's `case` syntax may seem arcane and bewildering at first, you get used to it quickly. You should definitely acquire a basic familiarity with the construct in order to be able to read scripts written by others, even if you don't plan to use it yourself. (Personally, I tend to favor it over `if [[ $string something...` in all its myriad slightly different variants. The POSIX compatibility is a nice bonus.) – tripleee Sep 03 '16 at 13:18
  • +1 Awesome solution, especially the case. One hint: pipes with functions are possible. You can use read inside the function to read or cat to read the input. – Daniel Bişar May 11 '20 at 18:48
  • @LLFourn @ Jo So - May I ask why quotes around $2 is not necessary? I think whenever content contains special characters or (more than one) spaces, we need the quotes otherwise it causes error. Thanks for this great answer. It is my favourite. – midnite Apr 13 '22 at 17:19
  • I know about when to use quotes in case statements now. https://unix.stackexchange.com/a/68748/150246 . May I ask why in your function `beginswith`, you can write `true` and `false` without echo. And they can be caught by the callers. I am trying to understand this syntax. Thank you. – midnite Apr 13 '22 at 19:21
  • Somehow from [this comment](https://stackoverflow.com/questions/5431909/returning-a-boolean-from-a-bash-function#comment21379873_5431932) I understand now. They are essentially the exit statuses, which are the last command in the function. A single `true` statement equals to `return 0`, while `false` equals to `return 1`. As these are exit statuses, they cannot be captured by variable assignment, nor by echo, etc. Yet they are cool for places which check the exit status, such as `if` and boolean expressions. Just want to quote [three ways for func](https://stackoverflow.com/a/8743103/1884546). – midnite Apr 13 '22 at 20:02
110

You can select just the part of the string you want to check:

if [ "${HOST:0:4}" = user ]

For your follow-up question, you could use an OR:

if [[ "$HOST" == user1 || "$HOST" == node* ]]
ErikE
  • 48,881
  • 23
  • 151
  • 196
martin clayton
  • 76,436
  • 32
  • 213
  • 198
65

I prefer the other methods already posted, but some people like to use:

case "$HOST" in 
    user1|node*) 
            echo "yes";;
        *)
            echo "no";;
esac

Edit:

I've added your alternates to the case statement above

In your edited version you have too many brackets. It should look like this:

if [[ $HOST == user1 || $HOST == node* ]];
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • Thanks, Dennis! I added to the original post a new question about how to combine expressions in if cluase. – Tim Jan 31 '10 at 16:45
  • 13
    "some people like..." : this one is more portable across versions and shells. – carlosayam Oct 17 '11 at 05:44
  • With case statements you can leave away the quotes around the variable since no word splitting occurs. I know it's pointless and inconsistent but I do like to leave away the quotes there to make it locally more visually appealing. – Jo So Nov 06 '14 at 15:12
  • And in my case, I had to leave away the quotes before ): "/*") did not work, /*) did. (I'm looking for strings starting with /, i.e., absolute paths) – Josiah Yoder Feb 18 '17 at 20:43
46

While I find most answers here quite correct, many of them contain unnecessary Bashisms. POSIX parameter expansion gives you all you need:

[ "${host#user}" != "${host}" ]

and

[ "${host#node}" != "${host}" ]

${var#expr} strips the smallest prefix matching expr from ${var} and returns that. Hence if ${host} does not start with user (node), ${host#user} (${host#node}) is the same as ${host}.

expr allows fnmatch() wildcards, thus ${host#node??} and friends also work.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
dhke
  • 15,008
  • 2
  • 39
  • 56
  • 3
    I'd argue that the bashism `[[ $host == user* ]]` might be necessary, since it's far more readable than `[ "${host#user}" != "${host}" ]`. As such granted that you control the environment where the script is executed (target the latest versions of `bash`), the former is preferable. – x-yuri Oct 24 '18 at 21:48
  • 3
    @x-yuri Frankly, I'd simply pack this away into a `has_prefix()` function and never look at it again. – dhke Oct 25 '18 at 10:29
40

Since # has a meaning in Bash, I got to the following solution.

In addition I like better to pack strings with "" to overcome spaces, etc.

A="#sdfs"
if [[ "$A" == "#"* ]];then
    echo "Skip comment line"
fi
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
ozma
  • 1,633
  • 1
  • 20
  • 28
16

Adding a tiny bit more syntax detail to Mark Rushakoff's highest rank answer.

The expression

$HOST == node*

Can also be written as

$HOST == "node"*

The effect is the same. Just make sure the wildcard is outside the quoted text. If the wildcard is inside the quotes it will be interpreted literally (i.e. not as a wildcard).

MrPotatoHead
  • 1,035
  • 14
  • 11
9

Keep it simple

word="appel"

if [[ $word = a* ]]
then
  echo "Starts with a"
else
  echo "No match"
fi
Thomas Van Holder
  • 1,137
  • 9
  • 12
8

@OP, for both your questions you can use case/esac:

string="node001"
case "$string" in
  node*) echo "found";;
  * ) echo "no node";;
esac

Second question

case "$HOST" in
 node*) echo "ok";;
 user) echo "ok";;
esac

case "$HOST" in
 node*|user) echo "ok";;
esac

Or Bash 4.0

case "$HOST" in
 user) ;&
 node*) echo "ok";;
esac
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
6
if [ [[ $HOST == user1 ]] -o [[ $HOST == node* ]] ];
then
echo yes
fi

doesn't work, because all of [, [[, and test recognize the same nonrecursive grammar. See section CONDITIONAL EXPRESSIONS on your Bash man page.

As an aside, the SUSv3 says

The KornShell-derived conditional command (double bracket [[]]) was removed from the shell command language description in an early proposal. Objections were raised that the real problem is misuse of the test command ([), and putting it into the shell is the wrong way to fix the problem. Instead, proper documentation and a new shell reserved word (!) are sufficient.

Tests that require multiple test operations can be done at the shell level using individual invocations of the test command and shell logicals, rather than using the error-prone -o flag of test.

You'd need to write it this way, but test doesn't support it:

if [ $HOST == user1 -o $HOST == node* ];
then
echo yes
fi

test uses = for string equality, and more importantly it doesn't support pattern matching.

case / esac has good support for pattern matching:

case $HOST in
user1|node*) echo yes ;;
esac

It has the added benefit that it doesn't depend on Bash, and the syntax is portable. From the Single Unix Specification, The Shell Command Language:

case word in
    [(]pattern1) compound-list;;
    [[(]pattern[ | pattern] ... ) compound-list;;] ...
    [[(]pattern[ | pattern] ... ) compound-list]
esac
Community
  • 1
  • 1
just somebody
  • 18,602
  • 6
  • 51
  • 60
  • 1
    `[` and `test` are Bash builtins as well as external programs. Try `type -a [`. – Dennis Williamson Feb 01 '10 at 12:05
  • Many thanks for explaining the problems with the "compound or", @just somebody - was looking precisely for something like that! Cheers! PS note (unrelated to OP): `if [ -z $aa -or -z $bb ]` ; ... gives "*bash: [: -or: binary operator expected*" ; however `if [ -z "$aa" -o -z "$bb" ] ; ...` passes. – sdaau Oct 23 '11 at 15:02
5

grep

Forgetting performance, this is POSIX and looks nicer than case solutions:

mystr="abcd"
if printf '%s' "$mystr" | grep -Eq '^ab'; then
  echo matches
fi

Explanation:

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
3

I tweaked @markrushakoff's answer to make it a callable function:

function yesNo {
  # Prompts user with $1, returns true if response starts with y or Y or is empty string
  read -e -p "
$1 [Y/n] " YN

  [[ "$YN" == y* || "$YN" == Y* || "$YN" == "" ]]
}

Use it like this:

$ if yesNo "asfd"; then echo "true"; else echo "false"; fi

asfd [Y/n] y
true

$ if yesNo "asfd"; then echo "true"; else echo "false"; fi

asfd [Y/n] Y
true

$ if yesNo "asfd"; then echo "true"; else echo "false"; fi

asfd [Y/n] yes
true

$ if yesNo "asfd"; then echo "true"; else echo "false"; fi

asfd [Y/n]
true

$ if yesNo "asfd"; then echo "true"; else echo "false"; fi

asfd [Y/n] n
false

$ if yesNo "asfd"; then echo "true"; else echo "false"; fi

asfd [Y/n] ddddd
false

Here is a more complex version that provides for a specified default value:

function toLowerCase {
  echo "$1" | tr '[:upper:]' '[:lower:]'
}

function yesNo {
  # $1: user prompt
  # $2: default value (assumed to be Y if not specified)
  # Prompts user with $1, using default value of $2, returns true if response starts with y or Y or is empty string

  local DEFAULT=yes
  if [ "$2" ]; then local DEFAULT="$( toLowerCase "$2" )"; fi
  if [[ "$DEFAULT" == y* ]]; then
    local PROMPT="[Y/n]"
  else
    local PROMPT="[y/N]"
  fi
  read -e -p "
$1 $PROMPT " YN

  YN="$( toLowerCase "$YN" )"
  { [ "$YN" == "" ] && [[ "$PROMPT" = *Y* ]]; } || [[ "$YN" = y* ]]
}

Use it like this:

$ if yesNo "asfd" n; then echo "true"; else echo "false"; fi

asfd [y/N]
false

$ if yesNo "asfd" n; then echo "true"; else echo "false"; fi

asfd [y/N] y
true

$ if yesNo "asfd" y; then echo "true"; else echo "false"; fi

asfd [Y/n] n
false
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mike Slinn
  • 7,705
  • 5
  • 51
  • 85