Proper Quoting in Bash: Attach numbers stored in i to a - linux

I wanted to write a short shell script, which removes specified pages from a pdf. Maybe I'm doing that in a bit convoluted manner, but that is what I came up with so far:
#!/bin/bash
#This is a script to remove a specified page from a specified pdf.
set verbose
s="A1-$(($2-1))"
if [ n -ge 3 ]; then
for i in 2..$#
do
s+=A$(($($i)+1))-$(($($(($i+1)))-1))
done
fi
pdftk A="$1" cat $s A$(($($#)+1))-end output output.pdf
I know it's quite convoluted code and if you know about the working of pdftk, I would appreciate a hint to make it easier, but for now I just need to know how to substitute a variable into a variable name. E.g. if
i=2
a2=3
echo $a($i)
gave me 3, that would be great, but it doesn't. How do I achieve this?

bash allows indirect parameter expansion:
$ i=2
$ a2=3
$ var="a$i" # a2
$ echo "${!var}"
3
What you really seem to want, though, is an array:
$ a=([2]=3) # Or simply a[2]=3
$ i=2
$ echo "${a[i]}"
3
(This is really a stop-gap answer, as there is almost certainly a much simpler answer to your question that doesn't involve this type of indirect parameter manipulation.)
I think this much simpler script that will do what you want:
#!/bin/bash
inputfile=$1
shift
ranges=() from=1
for pageToOmit in "$#"; do
ranges+=( "A$from-$(( pageToOmit - 1))" )
from=$(( pageToOmit + 1 ))
done
ranges+=( "$from-end" )
pdftk A="$inputfile" cat "${ranges[#]}" output output.pdf

Using eval:
i=2
a2=3
eval echo \$a$i
eval b=\$a$i
echo $b

Related

error using WHILE condition bash [duplicate]

This question already has answers here:
Print bash arguments in reverse order
(5 answers)
Closed 6 years ago.
I was trying to write a script that print the arguments in reverse order.
So if I type bash reverse.sh one two three
I expect my output to be three two one
How can i do this?
This is what I tried and it obviously didn't work...
#!/bin/bash
i=0
a="$"
for word in $*; do
echo $a$(($#-i))
i=$((i+1))
done
This is the output i get
$3
$2
$1
I thought this would print the parameters in order 3, 2, 1 but it didn't. How should I do it? Any help will be much appreciated. Thank you.
Let's define your arguments:
$ set -- one two three
Now, let's print them out in reverse order:
$ for ((i=$#;i>=1;i--)); do echo "${!i}"; done
three
two
one
How it works
for ((i=$#;i>=1;i--)) starts a loop in which i counts down from $# to 1. For each value of i, we print the corresponding positional parameter by ${!i}. The construct ${!i} uses indirection: instead of returning the value of i, ${!i} returns the value of the variable whose name is $i.
As a script
In a multi-line script form, we can use:
$ cat reverse
#!/bin/bash
for ((i=$#;i>=1;i--))
do
echo "${!i}"
done
As an example:
$ bash reverse One Two Three
Three
Two
One
Alternative: using tac
Another way to print things in reverse order is to use the utility tac. Consider this script:
$ cat reverse2
#!/bin/bash
printf "%s\n" "$#" | tac
Here is an example:
$ bash reverse2 Uno Dos Tres
Tres
Dos
Uno
printf "%s\n" "$#" prints out the positional parameters one per line. tac prints those lines in reverse order.
Limitation: The tac method only works correctly if the arguments do not themselves contain newlines.
You need eval with echo i.e. you need to evaluate the expansion, not output it:
eval echo $a$(($#-i))
Note that, using eval in general is discouraged as this could result in security implications if the input string is not sanitized. Check John1024's answer to see how this can be done without eval.

what did I do wrong in initializing my array with 0 ?

when I check the length of the array is always 1 even I give more parameters in the command line
for i in $*
do
echo $i
conect[$i]=0
done
echo ${#conect}
Try this:
#!/bin/bash
declare -A conect
for i in "$#"
do
echo $i
conect[$i]=0
done
echo ${#conect[#]}
Explanation:
An associative array (i.e. indexes can be non-numeric) must be declared with declare -A. You do not need this if indexes are guaranteed to be numeric.
${#foo} is the length (number of characters) of a string-valued variable; ${#conect[#]} is the length (number of elements) of an array.
As pointed out by others, "$#" is better than $*, especially when (quoted) parameters may contain spaces.
You should use an array:
for i in "$#"
$* create one single argument separated with IFS. that's why. Use $#
What is the difference between "$#" and "$*" in Bash?
Edit
Actually, as pointed out by #that_other_guy and #Ruud_Helderman (thanks to you both), what I said isn't quite right.
First thing is the Mea Culpa, as this matters isn't the full solution.
But it made me wonders so here is the correct way.
The IFS difference is a fact. But this solely matters if you quote "$*" or "$#"
for i in "$*"
do
echo $i
done
Will output every arguments on the same line whereas
for i in "$#"
do
echo $i
done
Will do it one at a time.

How to create a variable value using $ in the runtime in bash

I have a bash as below where i want to use the value of YYY_XXX_SK_REGISTER_CNTL in the echo .
#! /bin/bash
TRADE_TYPE=$1
YYY_XXX_SK_REGISTER_CNTL=YYY_XXX_SK_REGISTER_template.ctl
echo $TRADE_TYPE"_CTNL"
calling the base as below :
./test.sh YYY_XXX_SK_REGISTER
result expecting in echo : YYY_XXX_SK_REGISTER_template.ctl
If you don't mind changing TRADE_TYPE or using a temporary variable then you can use ${!var} expansion:
TRADE_TYPE="hello"
hello_world=1234
TRADE_TYPE="${TRADE_TYPE}_world"
echo ${!TRADE_TYPE}
# will print 1234
First you need to get rid of the typo, CNTL vs CTNL.
This script does what you want
#!/bin/bash
TRADE_TYPE=$1
YYY_XXX_SK_REGISTER_CNTL=YYY_XXX_SK_REGISTER_template.ctl
eval echo "\$${TRADE_TYPE}_CNTL"
There is probably a better solution than resorting to eval. But you will have to explain what your overall goal is.
I think you are expecting something like below code:
#!/bin/bash
TRADE_TYPE=$1
export TRADE_TYPE
variable="$TRADE_TYPE"_template.ctl
echo $variable
Using indirect variable reference:
#! /bin/bash
TRADE_TYPE="${1}_CNTL"
YYY_XXX_SK_REGISTER_CNTL=YYY_XXX_SK_REGISTER_template.ctl
echo "${!TRADE_TYPE}"
When the following is entered at the command line:
./test.sh YYY_XXX_SK_REGISTER
Your result will be:
YYY_XXX_SK_REGISTER_template.ctl

Looping through the elements of a path variable in Bash

I want to loop through a path list that I have gotten from an echo $VARIABLE command.
For example:
echo $MANPATH will return
/usr/lib:/usr/sfw/lib:/usr/info
So that is three different paths, each separated by a colon. I want to loop though each of those paths. Is there a way to do that? Thanks.
Thanks for all the replies so far, it looks like I actually don't need a loop after all. I just need a way to take out the colon so I can run one ls command on those three paths.
You can set the Internal Field Separator:
( IFS=:
for p in $MANPATH; do
echo "$p"
done
)
I used a subshell so the change in IFS is not reflected in my current shell.
The canonical way to do this, in Bash, is to use the read builtin appropriately:
IFS=: read -r -d '' -a path_array < <(printf '%s:\0' "$MANPATH")
This is the only robust solution: will do exactly what you want: split the string on the delimiter : and be safe with respect to spaces, newlines, and glob characters like *, [ ], etc. (unlike the other answers: they are all broken).
After this command, you'll have an array path_array, and you can loop on it:
for p in "${path_array[#]}"; do
printf '%s\n' "$p"
done
You can use Bash's pattern substitution parameter expansion to populate your loop variable. For example:
MANPATH=/usr/lib:/usr/sfw/lib:/usr/info
# Replace colons with spaces to create list.
for path in ${MANPATH//:/ }; do
echo "$path"
done
Note: Don't enclose the substitution expansion in quotes. You want the expanded values from MANPATH to be interpreted by the for-loop as separate words, rather than as a single string.
In this way you can safely go through the $PATH with a single loop, while $IFS will remain the same inside or outside the loop.
while IFS=: read -d: -r path; do # `$IFS` is only set for the `read` command
echo $path
done <<< "${PATH:+"${PATH}:"}" # append an extra ':' if `$PATH` is set
You can check the value of $IFS,
IFS='xxxxxxxx'
while IFS=: read -d: -r path; do
echo "${IFS}${path}"
done <<< "${PATH:+"${PATH}:"}"
and the output will be something like this.
xxxxxxxx/usr/local/bin
xxxxxxxx/usr/bin
xxxxxxxx/bin
Reference to another question on StackExchange.
for p in $(echo $MANPATH | tr ":" " ") ;do
echo $p
done
IFS=:
arr=(${MANPATH})
for path in "${arr[#]}" ; do # <- quotes required
echo $path
done
... it does take care of spaces :o) but also adds empty elements if you have something like:
:/usr/bin::/usr/lib:
... then index 0,2 will be empty (''), cannot say why index 4 isnt set at all
This can also be solved with Python, on the command line:
python -c "import os,sys;[os.system(' '.join(sys.argv[1:]).format(p)) for p in os.getenv('PATH').split(':')]" echo {}
Or as an alias:
alias foreachpath="python -c \"import os,sys;[os.system(' '.join(sys.argv[1:]).format(p)) for p in os.getenv('PATH').split(':')]\""
With example usage:
foreachpath echo {}
The advantage to this approach is that {} will be replaced by each path in succession. This can be used to construct all sorts of commands, for instance to list the size of all files and directories in the directories in $PATH. including directories with spaces in the name:
foreachpath 'for e in "{}"/*; do du -h "$e"; done'
Here is an example that shortens the length of the $PATH variable by creating symlinks to every file and directory in the $PATH in $HOME/.allbin. This is not useful for everyday usage, but may be useful if you get the too many arguments error message in a docker container, because bitbake uses the full $PATH as part of the command line...
mkdir -p "$HOME/.allbin"
python -c "import os,sys;[os.system(' '.join(sys.argv[1:]).format(p)) for p in os.getenv('PATH').split(':')]" 'for e in "{}"/*; do ln -sf "$e" "$HOME/.allbin/$(basename $e)"; done'
export PATH="$HOME/.allbin"
This should also, in theory, speed up regular shell usage and shell scripts, since there are fewer paths to search for every command that is executed. It is pretty hacky, though, so I don't recommend that anyone shorten their $PATH this way.
The foreachpath alias might come in handy, though.
Combining ideas from:
https://stackoverflow.com/a/29949759 - gniourf_gniourf
https://stackoverflow.com/a/31017384 - Yi H.
code:
PATHVAR='foo:bar baz:spam:eggs:' # demo path with space and empty
printf '%s:\0' "$PATHVAR" | while IFS=: read -d: -r p; do
echo $p
done | cat -n
output:
1 foo
2 bar baz
3 spam
4 eggs
5
You can use Bash's for X in ${} notation to accomplish this:
for p in ${PATH//:/$'\n'} ; do
echo $p;
done
OP's update wants to ls the resulting folders, and has pointed out that ls only requires a space-separated list.
ls $(echo $PATH | tr ':' ' ') is nice and simple and should fit the bill nicely.

Read values into a shell variable from a pipe

I am trying to get bash to process data from stdin that gets piped into, but no luck. What I mean is none of the following work:
echo "hello world" | test=($(< /dev/stdin)); echo test=$test
test=
echo "hello world" | read test; echo test=$test
test=
echo "hello world" | test=`cat`; echo test=$test
test=
where I want the output to be test=hello world. I've tried putting "" quotes around "$test" that doesn't work either.
Use
IFS= read var << EOF
$(foo)
EOF
You can trick read into accepting from a pipe like this:
echo "hello world" | { read test; echo test=$test; }
or even write a function like this:
read_from_pipe() { read "$#" <&0; }
But there's no point - your variable assignments may not last! A pipeline may spawn a subshell, where the environment is inherited by value, not by reference. This is why read doesn't bother with input from a pipe - it's undefined.
FYI, http://www.etalabs.net/sh_tricks.html is a nifty collection of the cruft necessary to fight the oddities and incompatibilities of bourne shells, sh.
if you want to read in lots of data and work on each line separately you could use something like this:
cat myFile | while read x ; do echo $x ; done
if you want to split the lines up into multiple words you can use multiple variables in place of x like this:
cat myFile | while read x y ; do echo $y $x ; done
alternatively:
while read x y ; do echo $y $x ; done < myFile
But as soon as you start to want to do anything really clever with this sort of thing you're better going for some scripting language like perl where you could try something like this:
perl -ane 'print "$F[0]\n"' < myFile
There's a fairly steep learning curve with perl (or I guess any of these languages) but you'll find it a lot easier in the long run if you want to do anything but the simplest of scripts. I'd recommend the Perl Cookbook and, of course, The Perl Programming Language by Larry Wall et al.
This is another option
$ read test < <(echo hello world)
$ echo $test
hello world
read won't read from a pipe (or possibly the result is lost because the pipe creates a subshell). You can, however, use a here string in Bash:
$ read a b c <<< $(echo 1 2 3)
$ echo $a $b $c
1 2 3
But see #chepner's answer for information about lastpipe.
I'm no expert in Bash, but I wonder why this hasn't been proposed:
stdin=$(cat)
echo "$stdin"
One-liner proof that it works for me:
$ fortune | eval 'stdin=$(cat); echo "$stdin"'
bash 4.2 introduces the lastpipe option, which allows your code to work as written, by executing the last command in a pipeline in the current shell, rather than a subshell.
shopt -s lastpipe
echo "hello world" | read test; echo test=$test
A smart script that can both read data from PIPE and command line arguments:
#!/bin/bash
if [[ -p /dev/stdin ]]
then
PIPE=$(cat -)
echo "PIPE=$PIPE"
fi
echo "ARGS=$#"
Output:
$ bash test arg1 arg2
ARGS=arg1 arg2
$ echo pipe_data1 | bash test arg1 arg2
PIPE=pipe_data1
ARGS=arg1 arg2
Explanation: When a script receives any data via pipe, then the /dev/stdin (or /proc/self/fd/0) will be a symlink to a pipe.
/proc/self/fd/0 -> pipe:[155938]
If not, it will point to the current terminal:
/proc/self/fd/0 -> /dev/pts/5
The bash [[ -p option can check it it is a pipe or not.
cat - reads the from stdin.
If we use cat - when there is no stdin, it will wait forever, that is why we put it inside the if condition.
The syntax for an implicit pipe from a shell command into a bash variable is
var=$(command)
or
var=`command`
In your examples, you are piping data to an assignment statement, which does not expect any input.
In my eyes the best way to read from stdin in bash is the following one, which also lets you work on the lines before the input ends:
while read LINE; do
echo $LINE
done < /dev/stdin
The first attempt was pretty close. This variation should work:
echo "hello world" | { test=$(< /dev/stdin); echo "test=$test"; };
and the output is:
test=hello world
You need braces after the pipe to enclose the assignment to test and the echo.
Without the braces, the assignment to test (after the pipe) is in one shell, and the echo "test=$test" is in a separate shell which doesn't know about that assignment. That's why you were getting "test=" in the output instead of "test=hello world".
Because I fall for it, I would like to drop a note.
I found this thread, because I have to rewrite an old sh script
to be POSIX compatible.
This basically means to circumvent the pipe/subshell problem introduced by POSIX by rewriting code like this:
some_command | read a b c
into:
read a b c << EOF
$(some_command)
EOF
And code like this:
some_command |
while read a b c; do
# something
done
into:
while read a b c; do
# something
done << EOF
$(some_command)
EOF
But the latter does not behave the same on empty input.
With the old notation the while loop is not entered on empty input,
but in POSIX notation it is!
I think it's due to the newline before EOF,
which cannot be ommitted.
The POSIX code which behaves more like the old notation
looks like this:
while read a b c; do
case $a in ("") break; esac
# something
done << EOF
$(some_command)
EOF
In most cases this should be good enough.
But unfortunately this still behaves not exactly like the old notation
if some_command prints an empty line.
In the old notation the while body is executed
and in POSIX notation we break in front of the body.
An approach to fix this might look like this:
while read a b c; do
case $a in ("something_guaranteed_not_to_be_printed_by_some_command") break; esac
# something
done << EOF
$(some_command)
echo "something_guaranteed_not_to_be_printed_by_some_command"
EOF
Piping something into an expression involving an assignment doesn't behave like that.
Instead, try:
test=$(echo "hello world"); echo test=$test
The following code:
echo "hello world" | ( test=($(< /dev/stdin)); echo test=$test )
will work too, but it will open another new sub-shell after the pipe, where
echo "hello world" | { test=($(< /dev/stdin)); echo test=$test; }
won't.
I had to disable job control to make use of chepnars' method (I was running this command from terminal):
set +m;shopt -s lastpipe
echo "hello world" | read test; echo test=$test
echo "hello world" | test="$(</dev/stdin)"; echo test=$test
Bash Manual says:
lastpipe
If set, and job control is not active, the shell runs the last command
of a pipeline not executed in the background in the current shell
environment.
Note: job control is turned off by default in a non-interactive shell and thus you don't need the set +m inside a script.
I think you were trying to write a shell script which could take input from stdin.
but while you are trying it to do it inline, you got lost trying to create that test= variable.
I think it does not make much sense to do it inline, and that's why it does not work the way you expect.
I was trying to reduce
$( ... | head -n $X | tail -n 1 )
to get a specific line from various input.
so I could type...
cat program_file.c | line 34
so I need a small shell program able to read from stdin. like you do.
22:14 ~ $ cat ~/bin/line
#!/bin/sh
if [ $# -ne 1 ]; then echo enter a line number to display; exit; fi
cat | head -n $1 | tail -n 1
22:16 ~ $
there you go.
The questions is how to catch output from a command to save in variable(s) for use later in a script. I might repeat some earlier answers but I try to line up all the answers I can think up to compare and comment, so bear with me.
The intuitive construct
echo test | read x
echo x=$x
is valid in Korn shell because ksh have implemented that the last command in a piped series is part of the current shell ie. the previous pipe commands are subshells. In contrast other shells define all piped commands as subshells including the last.
This is the exact reason I prefer ksh.
But having to copy with other shells, bash f.ex., another construct must be used.
To catch 1 value this construct is viable:
x=$(echo test)
echo x=$x
But that only caters for 1 value to be collected for later use.
To catch more values this construct is useful and works in bash and ksh:
read x y <<< $(echo test again)
echo x=$x y=$y
There is a variant which I have noticed work in bash but not in ksh:
read x y < <(echo test again)
echo x=$x y=$y
The <<< $(...) is a here-document variant which gives all the meta handling of a standard command line. < <(...) is an input redirection of a file-substitution operator.
I use "<<< $(" in all my scripts now because it seems the most portable construct between shell variants. I have a tools set I carry around on jobs in any Unix flavor.
Of course there is the universally viable but crude solution:
command-1 | {command-2; echo "x=test; y=again" > file.tmp; chmod 700 file.tmp}
. ./file.tmp
rm file.tmp
echo x=$x y=$y
I wanted something similar - a function that parses a string that can be passed as a parameter or piped.
I came up with a solution as below (works as #!/bin/sh and as #!/bin/bash)
#!/bin/sh
set -eu
my_func() {
local content=""
# if the first param is an empty string or is not set
if [ -z ${1+x} ]; then
# read content from a pipe if passed or from a user input if not passed
while read line; do content="${content}$line"; done < /dev/stdin
# first param was set (it may be an empty string)
else
content="$1"
fi
echo "Content: '$content'";
}
printf "0. $(my_func "")\n"
printf "1. $(my_func "one")\n"
printf "2. $(echo "two" | my_func)\n"
printf "3. $(my_func)\n"
printf "End\n"
Outputs:
0. Content: ''
1. Content: 'one'
2. Content: 'two'
typed text
3. Content: 'typed text'
End
For the last case (3.) you need to type, hit enter and CTRL+D to end the input.
How about this:
echo "hello world" | echo test=$(cat)

Resources