Another way to do this thing is to name the parameter you want to declare and then do so:
{ cat >./foo
chmod +x ./foo
eight=declared ./foo
eight=declared_and_preferred \
./foo 1 2 3 4 5 6 7 8
./foo 1 2 3 4 5 6 7 8
./foo
} <<\SCRIPT
#!/usr/bin/sh
: ${eight:=${8:-some_default}}
printf '$eight = %s\n' "$eight"
#END
SCRIPT
OUTPUT
$eight = declared
$eight = declared_and_preferred
$eight = 8
$eight = some_default
In the above example the explicitly declared environment variable is preferred to the command-line argument, but the command-line argument is used when the environment variable is either empty or unset. When both the 8th positional and the environment variable $eight
are empty or unset the default value some_default is assigned to $eight
. In either case the :
can be removed from the :-
or :=
statements if empty should be an acceptable value.
The variable $eight
could as well have been set like:
printf '$eight = %s\n' "${eight:=${8:-some_default}}"
... and the previous line omitted entirely but I wanted to demonstrate that declaring a variable in that way does result in a persistent value, and so I did it in two commands. Either way $eight
is set to the final value of that compound parameter expansion.
getopts
- for robust scripts - often is the best way to handle command options. Positional parameters, on the other hand, are almost always the most simple means of robustly handling operands in a script.
For example:
touch file$(seq -ws\ file 100)
./foo *
OUTPUT
$eight = file008
There we only see the eighth operand, but there are 101 of them globbed from my test directory.
All examples presented below work for general case where there's an arbitrary number of words on the line. The essential idea is the same everywhere - we have to read the file line by line and print the words in reverse. AWK facilitates this the best because it already has all the necessary tools for text processing done programmatically, and is most portable - it can be used with any awk derivative, and most systems have it. Python also has quite a few good utilities for string processing that allow us to do the job. It's a tool for more modern systems, I'd say. Bash, IMHO, is the least desirable approach, due to portability, potential hazards, and the amount of "trickery" that needs to be done.
AWK
$ awk '{for(i=NF;i>=1;i--) printf "%s ", $i;print ""}' input.txt
Earth Hello
Mars Hello
The way this works is fairly simple: we're looping backwards through each word on the line, printing words separated with space - that's done by printf "%s ",$i
function (for printing formatted strings) and for-loop. NF
variable corresponds to number of fields. The default field separator is assumed to be space. We start by setting a throw-away variable i
to the number of words, and on each iteration, decrement the variable. Thus, if there's 3 words on line, we print field $3, then $2, and $1. After the last pass, variable i becomes 0, the condition i>=1
becomes false, and the loop terminates. To prevent lines being spliced together, we insert a newline using print ""
. AWK code blocks {}
are processed for each line in this case (if there's a matching condition in front of code block, it depends on the match for the code block to be executed or not).
Python
For those who like alternative solutions, here's python:
$ python -c "import sys;print '\n'.join([ ' '.join(line.split()[::-1]) for line in sys.stdin ])" < input.txt
Earth Hello
Mars Hello
The idea here is slightly different. <
operator tells your current shell to redirect input.txt
into python's stdin
stream, and we read that line by line. Here we use list comprehension to create a list of lines - that's what the [ ' '.join(line.split()[::-1]) for line in sys.stdin ]
part does. The part ' '.join(line.split()[::-1])
takes a line, splits it into list of words, reverses the list via [::-1]
, and then ' '.join()
creates a space-separated string out of it. We have as a result a list of larger strings. Finally, '\n'.join()
makes an even larger string, with each item joined via newline.
In short, this method is basically a "break and rebuild" approach.
BASH
#!/bin/bash
while IFS= read -r line
do
bash -c 'i=$#; while [ $i -gt 0 ];do printf "%s " ${!i}; i=$(($i-1)); done' sh $line
echo
done < input.txt
And a test run:
$ ./reverse_words.sh
Earth Hello
Mars Hello
Bash itself doesn't have strong text processing capabilities. What happens here is that we read the file line by line via
while IFS= read -r line
do
# some code
done < text.txt
This is a frequent technique and is widely used in shell scripting to read output of a command or a text file line-by-line. Each line is stored into $line
variable.
On the inside we have
bash -c 'i=$#; while [ $i -gt 0 ];do printf "%s " ${!i}; i=$(($i-1)); done' sh $line
Here we use bash
with -c
flag to run a set of commands enclosed into single-quotes. When -c
is used, bash
will start assigning command-line arguments into variables starting with $0
. Because that $0
is traditionally used to signify a program's name, I use sh
dummy variable first.
The unquoted $line
will be broken down into individual items due to the behavior known as word-splitting. Word splitting is often undesirable in shell scripting, and you will often hear people say "always quote your variables, like "$foo"." In this case, however, word-splitting is desirable for processing simple text. If your text contains something like $var
, it might break this approach. For this, and several other reasons, I'd say python and awk approach are better.
As for the inner code, it's also simple: the unquoted $line
is split into words and is passed to the inner code for processing. We take the number of arguments $#
, store it into the throw away variable i
, and again - print out each item using something known as variable indirection - that's the ${!i}
part (note that this is bashism - it's not available in other shells). And again, we use printf "%s "
to print out each word, space-separated. Once that's done, echo
will append a newline.
Essentially this approach is a mix of both awk and python. We read the file line by line, but divide and conquer each line, using several of bash
's features to do the job.
A simpler variation can be done with the GNU tac
command, and again playing with word splitting. tac
is used to reverse lines of input stream or file, but in this case we specify -s " "
to use space as separator. Thus, var
will contain a newline-separated list of words in reverse order, but due to $var
not being quoted, newline will be substituted with space. Trickery, and again not the most reliable, but works.
#!/bin/bash
while IFS= read -r line
do
var=$(tac -s " " <<< "$line" )
echo $var
done < input.txt
Test runs:
And here's the 3 methods with arbitrary lines of input
$ cat input.txt
Hello Earth end of line
Hello Mars another end of line
abra cadabra magic
$ ./reverse_words.sh
line of end Earth Hello
line of end another Mars Hello
magic cadabra abra
$ python -c "import sys;print '\n'.join([ ' '.join(line.split()[::-1]) for line in sys.stdin ])" < input.txt
line of end Earth Hello
line of end another Mars Hello
magic cadabra abra
$ awk '{for(i=NF;i>=1;i--) printf "%s ", $i;print ""}' input.txt
line of end Earth Hello
line of end another Mars Hello
magic cadabra abra
Extra: perl and ruby
Same idea as with python - we split each line into array of words, reverse the array, and print it out.
$ perl -lane '@r=reverse(@F); print "@r"' input.txt
line of end Earth Hello
line of end another Mars Hello
magic cadabra abra
$ ruby -ne 'puts $_.chomp.split().reverse.join(" ")' < input.txt
line of end Earth Hello
line of end another Mars Hello
magic cadabra abra
Best Answer
eval
is the only portable way to access a positional parameter by its dynamically-chosen position. Your script would be clearer if you explicitly looped on the index rather than the values (which you aren't using). Note that you don't needexpr
unless you want your script to run in antique Bourne shells;$((…))
arithmetic is in POSIX. Limit the use ofeval
to the smallest possible fragment; for example, don't useeval echo
, assign the value to a temporary variable.In bash, you can use
${!i}
to mean the value of the parameter whose name is$i
. This works when$i
is either a named parameter or a number (denoting a positional parameter). While you're at it, you can make use of other bash convenience features.