0

This question differs in that the classic "use a function" answer WILL NOT work. Adding a note to an existing Alias question is equivalent to sending a suggestion e-mail to Yahoo.

I am trying to write macros to get around BASH's horrendous IF syntax. You know, the [, [[, ((...BASH: the PHP of flow control...just add another bracket. I'm still waiting for the "(((((((" form. Not quite sure why BASH didn't repurpose "(", as "(" has no real semantics at the if statement.

The idea is to have named aliases for [, [[ and (( , as each one of these durned test-ish functions has a frustratingly different syntax. I honestly can never remember which is which (how COULD you? It's completely ad hoc!), and good luck trying to google "[[".

I would then use the names as a mnemonic, and the alias to get rid of the completely awful differences in spacing requirements. Examples: "whatdoyoucallthisIf" for "((", "shif" (for shell if), "mysterydoublesquarebacketif" for that awful [[ thing which seems to mostly do the same thing as [, only it doesn't.

Thus, I MUST have something of the form:

alias IFREPLACEMENT="if [ \$@ ]; then"

But obviously not \$@, which would just cement in the current argument list to the shell running the alias.

Functions will not work in this case, as the function:

function IFREPLACEMENT {
    if [ $@ ]; then
}

is illegal.

In CSH, you could say alias abc blah blah !* !1, etc. Is there ANYTHING in BASH that is similar (no, !* doesn't work in BASH)?

Or am [ "I just out of luck" ]; ?

As an aside, here are some of the frustrating differences involving test-ish functions in BASH that I am trying to avoid by using well-defined aliases that people would have to use instead of picking the wrong "[[", "[" or "((":

  • "((" is really creepy...if a variable contains the name of another variable, it's derferenced for as many levels as necessary)
  • "((" doesn't require a spaces like '[' and '[['
  • "((" doesn't require "$" for variables to be dereferenced
  • ['s "-gt" is numeric or die. [[ seems to have arbitrary restrictions.
  • '[' and '[[' use ">" (etc) as LEXICAL comparison operators, but they have frustratingly different rules that make it LOOK like they're doing numeric comparisons when they really aren't.
  • for a variable: a="" (empty value), [ $a == 123 ] is a syntax error, but (( a == 123 )) isn't.
mklement0
  • 382,024
  • 64
  • 607
  • 775
Mark Gerolimatos
  • 2,424
  • 1
  • 23
  • 33
  • 7
    Yes, Bash syntax and expansion rules are disgusting. Attempting to alias stuff away (even if you could get it to work) will not make this better, though. You'd simply be adding a further level of confusion that the maintainer of the script would have to work through. – Oliver Charlesworth Jun 12 '14 at 21:05
  • 4
    They're not that confusing. `(( ))` is mainly for arithmetic operations while `[[ ]]` is mainly for general testing. And just think of `[[ ]]` as an extended and safer version of `[ ]`. – konsolebox Jun 12 '14 at 21:08
  • Agreed, this is not a good idea. Bash syntactic sugar coded in bash sounds... bug-ridden. Better to learn the proper `[[` way of doing things. – Conner Jun 12 '14 at 21:08
  • Perhaps you're doing it wrong since you're trying to inject non-arithmetic operations to `(( ))`. – konsolebox Jun 12 '14 at 21:10
  • 3
    Bash's `if` syntax is `if commands; then commands; fi`. None of the brackets you call "horrendous IF syntax" is `if` syntax. – that other guy Jun 12 '14 at 21:15
  • Following @OliCharlesworth and @Conner advise, BASH if syntax is really pretty simple. Use `[[` which itself is essentially a builtin of `test`. There are only a very few corner cases `[[` does not cover that `[` will, so as a general rule, just make friends with `[[`. For maximum portability, just use `test` (remember to double-quote the arguments) and you are fine. The only corner case covered by `[[` not covered by `test` is the `=~`substring operator. Rather than fight with `[[`, just make friends with it -- life will be much better... – David C. Rankin Jun 12 '14 at 21:16
  • 1
    Actually it's not only `=~` that `[[ ]]` has but `==`, `&&`, `||` and `()` as well. – konsolebox Jun 12 '14 at 21:19
  • @konsolebox: `[` has `-a` for `&&` and `-o` for `||` and `\(` and `\)` for `()`. The `==` operator does some weird pattern-matchy stuff, IIRC; I don't use it enough to have memorized it because `[` does what I want most of the time. – Jonathan Leffler Jun 12 '14 at 21:22
  • 2
    It sounds like you need to write your own shell or find some other shell that meets your requirements. Trying to use `alias` for the job is worse than the problem you're purporting to fix. The `alias` mechanism simply has no way to provide arguments in the form you want them provided -- it is unconditionally the wrong tool for the job. I'm not convinced that functions could help you either, but that's a separate issue ruled out by the terms of the question. – Jonathan Leffler Jun 12 '14 at 21:28
  • @JonathanLeffler Good point. Never thought about quoting () actually. Only that && and || looks more readable and that you don't have to quote (). `==` is as good as =~ and could be even better for those who know how to use extended pattern matching. `[[ ]]` is also way faster than `[ ]` or `test` since it's more like a direct parser. – konsolebox Jun 12 '14 at 21:30
  • Consider picking a good scripting language; it doesn't have to be a shell. I like Perl myself, but you'd probably be shouting about it in all-caps very quickly. You might consider Python. – Keith Thompson Jun 12 '14 at 21:37
  • @konsolebox: Actually, `[` is _also_ a `bash` builtin (run `type [`). – mklement0 Jun 12 '14 at 22:19
  • @mklement0 I didn't say it isn't. Yes `[` and `test` are shell builtins but unlike them `[[ ]]` is special. – konsolebox Jun 13 '14 at 07:51
  • # time for I in {1..100000}; do test "$RANDOM" -eq "$RANDOM"; done >>> real 0m0.711s # time for I in {1..100000}; do [[ "$RANDOM" -eq "$RANDOM" ]]; done >>> real 0m0.487s # time for I in {1..100000}; do [[ RANDOM -eq RANDOM ]]; done >>> real 0m0.408s – konsolebox Jun 13 '14 at 07:55
  • These stuffs are known for ages since 3.0 or 2.05b. – konsolebox Jun 13 '14 at 07:58
  • @konsolebox: Thanks for clearing that up: I was confused about shell _builtin_ vs. shell _keyword_. Great tests (though you accidentally tested `[[` twice). I ran some tests of my own: You were absolutely right. `[[` is _almost twice as fast_ as `[`/`test`. Other findings: `((` is about 15-20% slower than `[[` (but still much faster than `[`/`test`). Curiously, `test` is slightly faster than `[`, even though the 2 should really be the same. bash performance improved over time: bash 4.3.11 ran all tests about 20% faster than bash 3.2.51. – mklement0 Jun 13 '14 at 16:26
  • I really intended to do `[[` twice to show that it also has the advantage of being able to reference a variable's value directly without needing to depend on the expansion of `$`. And it's faster than when referencing with `$`. `test` probably goes faster than `[` since `[` gets an extra argument that's passed to it and also same argument that it needs to parse (`]`). – konsolebox Jun 13 '14 at 17:30
  • @konsolebox Thanks; quite the learning experience for me: had no idea that you can omit the `$` in variable references with _numerical_ operators (and you can even use _full arithmetic expression, including assignments as operands_, unquoted, if without whitespace, or quoted, with whitespace). However, your findings only apply to _double-quoted_ `$`-prefixed variable references; if you use them _unquoted_, you'll find that _they are faster_. Verify with the updated test code at the end of my answer. My guess: not using `$` gets arithmetic evaluation involved, which is costly. – mklement0 Jun 13 '14 at 19:24
  • For those who are asking why I am doing this...It's not for speed (although that's a good thing!), nor to fix anything in BASH per se. The problem is that I have seen scripts break in the middle of the night during crucial events because someone innocently used '[' instead of '[[' or '((" (or the opposite). To prevent people from having to remember which bracket to use, I would then have a NUMERIC-IF and a LEXICAL-IF. Since these are operational pages, speed is a secondary issue. Predictability is the #1 goal here. – Mark Gerolimatos Jun 14 '14 at 01:41

2 Answers2

3

Sure, functions will work, but not like a macro:

function IFREPLACEMENT {
     [[ "$@" ]]
}

IFREPLACEMENT "$x" = "$y" && { 
    echo "the same
}

FWIW, here's a brutal way to pass arguments to an alias.

$ alias enumerate='bash -c '\''for ((i=0; i<=$#; i++)); do arg=${!i}; echo $i $arg; done'\'
$ enumerate foo bar baz
0 foo
1 bar
2 baz

Clearly, because a new bash shell is spawned, whatever you do won't have any effect on the current shell.

mklement0
  • 382,024
  • 64
  • 607
  • 775
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • The `alias` hack is clever, but to add to the caveats: it's inefficient, and the code in the alias also won't _see_ the current shell's variables and state (in addition to not being able to _affect_ it). – mklement0 Jun 13 '14 at 19:35
3

Update: Based on feedback from @konsolebox, the recommendation is now to always use [[...]] for both simplicity and performance (the original answer recommended ((...)) for numerical/Boolean tests).


@Oliver Charlesworth, in a comment on the question, makes the case for not trying to hide the underlying bash syntax - and I agree.

You can simplify things with the following rules:

  • Always use [[ ... ]] for tests.
    • Only use [ ... ] if POSIX compatibility is a must. If available, [[ ... ]] is always the better choice (fewer surprises, more features, and almost twice as fast[1]).
    • Use double-quoted, $-prefixed variable references - for robustness and simplicity (you do pay a slight performance penalty for double-quoting, though1) - e.g., "$var"; see the exceptions re the RHS of == and =~ below.
  • Whitespace rules:
    • ALWAYS put a space after the initial delimiter and before the closing delimiter of conditionals (whether [[ / (( or ]] / )))
    • NEVER put spaces around = in variable assignments.

These rules are more restrictive than they need to be - in the interest of simplification.

Tips and pitfalls:

  • Note that for numeric comparison with [[ ... ]], you must use -eq, -gt, -ge, -lt, -le, because ==, <, <=, >, >= are for lexical comparison.
    • [[ 110 -gt 2 ]] && echo YES
  • If you want to use == with pattern matching (globbing), either specify the entire RHS as an unquoted string, or, at least leave the special globbing characters unquoted.
    • [[ 'abc' == 'a'* ]] && echo YES
  • Similarly, performing regex matching with =~ requires that either the entire RHS be unquoted, or at least leave the special regex chars. unquoted - if you use a variable to store the regex - as you may have to in order to avoid bugs with respect to \-prefixed constructs on Linux - reference that variable unquoted.
    • [[ 'abc' =~ ^'a'.+$ ]] && echo YES
    • re='^a.+$'; [[ 'abc' =~ $re ]] && echo YES # *unquoted* use of var. $re
  • An alternative to [[ ... ]], for purely numerical/Boolean tests, is to use arithmetic evaluation, ((...)), whose performance is comparable to [[ (about 15-20% slower1); arithmetic evaluation (see section ARITHMETIC EVALUATION in man bash):

    • Allows C-style arithmetic (integer) operations such as +, -, *, /, **, %, ...
    • Supports assignments, including increment and decrement operations (++ / --).
    • No $ prefix required for variable references.

      • Caveat: You still need the $ in 2 scenarios:
        • If you want to specify a number base or perform up-front parameter expansion, such as removing a prefix:
          • var=010; (( 10#$var > 9 )) && echo YES # mandate number base 10
          • var=v10; (( ${var#v} > 9 )) && echo YES # strip initial 'v'
        • If you want to prevent recursive variable expansion.
          • ((...), curiously, expands a variable name without $ recursively, until its value is not the name of an existing variable anymore:
          • var1=10; var2=var1; (( var2 > 9 )) && echo YES
          • var2 expands to 10(!)
    • Has laxer whitespace rules.

    • Example: v1=0; ((v2 = 1 + ++v1)) && echo YES # -> $v1 == 1, $v2 == 2
    • Caveat: Since arithmetic evaluation behaves so differently from the rest of bash, you'll have to weigh its added features against having to remember an extra set of rules. You also pay a slight performance penalty1.
    • You can even cram arithmetic expressions, including assignments, into [[ conditionals that are based on numeric operators, though that may get even more confusing; e.g.:

      v1=1 v2=3; [[ v1+=1 -eq --v2 ]] && echo TRUE # -> both $v1 and $v2 == 2
      

Note: In this context, by 'quoting' I mean single- or double-quoting an entire string, as opposed to \-escaping individual characters in a string not enclosed in either single- or double quotes.


1: The following code - adapted from code by @konsolebox - was used for performance measurements:

Note:

  • The results can vary by platform - numbers are based on OS X 10.9.3 and Ubuntu 12.04.
  • [[ being nearly twice as fast as [ (factor around 1.9) is based on:
    • using unquoted, $-prefixed variable references in [[ (using double-quoted variable references slows things down somewhat)
  • (( is slower than [[ with unquoted, $-prefixed variable on both platforms: about 15-20% on OSX, around 30% on Ubuntu. On OSX, using double-quoted, $-prefixed variable references is actually slower, as is not using the $ prefix at all (works with numeric operators). By contrast, on Ubuntu, (( is slower than all ]] variants.
#!/usr/bin/env bash

headers=( 'test' '[' '[[/unquoted' '[[/quoted' '[[/arithmetic' '((' )
iterator=$(seq 100000)
{
time for i in $iterator; do test "$RANDOM" -eq "$RANDOM"; done
time for i in $iterator; do [ "$RANDOM" -eq "$RANDOM" ]; done
time for i in $iterator; do [[ $RANDOM -eq $RANDOM ]]; done
time for i in $iterator; do [[ "$RANDOM" -eq "$RANDOM" ]]; done
time for i in $iterator; do [[ RANDOM -eq RANDOM ]]; done
time for i in $iterator; do (( RANDOM == RANDOM )); done
} 2>&1 | fgrep 'real' | { i=0; while read -r line; do echo "${headers[i++]}: $line"; done; } | sort -bn -k3.3 | awk 'NR==1 { baseTime=substr($3,3) } { time=substr($3,3); printf "%s %s%%\n", $0, (time/baseTime)*100 }' | column -t

Outputs times from fastest to slowest, with slower times also expressed as a percentage of the fastest time.

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Funny; the reason I avoid `[[` is all the unexpected weird extra things it does compared with the nice simple `[` command syntax. – Jonathan Leffler Jun 12 '14 at 21:48
  • @JonathanLeffler: I frequently _use_ the extra things it offers, and I like (mostly) not having to double-quote variable references, and the availability of `&&` and `||` - I do see, however, how the subtleties and deviations from behavior elsewhere can be confusing and hard to remember. – mklement0 Jun 12 '14 at 21:54
  • Something along the lines of 'each to their own' and 'special cases are the enemy of simplicity'. When I learned shell, `[` wasn't a built-in (yes, it was a long time ago!) but a regular command. The built-in optimization happened a couple of years later (nothing to do with me, though). – Jonathan Leffler Jun 12 '14 at 21:58
  • @JonathanLeffler: `'special cases are the enemy of simplicity'` - fully agreed. When I learned shell, there were only _batch_ files (in my world, at least :) Thanks for your edit. – mklement0 Jun 12 '14 at 22:15
  • 1
    You had my +1 but I don't fully agree with the rule of choosing `(( ))` over `[[ ]]` with numeric comparisons. A scripter can opt to choose `[[ ]]` as a general tool for testing may it be lexical or numerical, even if `(( ))` is capable of simplifying things with arithmetic operations before comparisons. You still can't avoid cases where you need to do string and numerical comparisons at the same time e.g. `[[ -n $X && X -eq 0 ]]`. If you're concerned about uniformity in code this is something to consider. – konsolebox Jun 13 '14 at 08:42
  • And I'm someone who gives more favor to `[[ ]]` even if I do stuffs like `(( ++COUNTER[$I] > 2 ))` when needed. As you may have known none in Bash manual gives favor to `(( ))` as a general testing tool for numbers; even yet `(( ))` is under ARITHMETIC EVALUATION; whereas `[[ ]]` is under CONDITIONAL EXPRESSIONS. – konsolebox Jun 13 '14 at 08:43
  • @konsolebox Thanks for all your feedback - great points. I've updated my answer. – mklement0 Jun 13 '14 at 17:00
  • 1
    @konsolebox, if you need to lexical and numeric, you *could* do two separate commands: `[[ -n $X ]] && (( X == 0 ))`. – glenn jackman Jun 13 '14 at 19:49
  • 3
    IMO, if you're worried about the performance of `[[` vs `((`,a shell language is the wrong tool for the job. – glenn jackman Jun 13 '14 at 19:52
  • 1
    @glennjackman I'd take that as a matter of your preference or opinion. If Bash is still the best tool for the job, is it wrong to care about efficiency? And using `[[ ]]` and `(( ))` together adds complexity - even if it was obvious. – konsolebox Jun 13 '14 at 20:11