In bash, this syntax can be used to get list of command line arguments, starting from $2:
echo "${#:2}"
This syntax does not seem to work in sh (/bin/dash).
What would be the best way to emulate this in sh ?
(shift; echo "$#")
Using the subshell created by the parens ensures that "$#" in the outer scope is not modified.
As another approach (uglier, but avoiding the need for a subshell), you can remove the first argument, and re-add it later:
argOne=$1 # put $1 in $argOne
shift # rename $2 to $1, $3 to $2, etc
echo "$#" # Pass the new argument list to echo
set -- "$argOne" "$#" # Create a new argument list, restoring argOne before the rest
Related
Given the following Bash shell script excerpt:
# The intent is to take the PATH env variable, break it up into its parts, making them
# appear to be command line args (i.e., `$1`, `$2`, ...), and then for this example, just
# echo the parts in space delimited form, but we can imagine that we may want to do other
# things with them - this is just sample usage
# Important Requirement/Constraint
# ================================
# Please do not alter the "PATH to $1, $2, $3, ..." portion of the answer or replace the
# Bash ".." range construct with the output of the "seq" command exec'd in a subshell.
# Preferably, the answer should simply consist of the simplification of the last line of
# code - the "eval eval ..." . Also, please don't simplify by collapsing the whole thing
# to just echo "$#" since we may want to work with only some of the parts, and not
# necessarily the first parts, of the path. That is to say that the 1 and $# in the
# {1..$#} range could be replaced with other shell variables or expr., potentially
# Test case
PATH=/usr/local/bin:/usr/bin:/bin
# The code being examined follows
# Set ':' as the input field separator of the path
IFS=: # Or, more appropriately if in a function: local IFS=:
# Parse the PATH environment variable and break it up into its components
set $PATH
# This is the line we want to simplify, if possible, without losing functionality of
# course (see the comment that follows for details)
eval eval echo '\'$(eval 'echo "\${1..$#}"')
# Some notes and explanations regarding the functionality and underlying intent of the
# preceding line:
# - We start by dynamically creating the following construct: ${1..3}
# since $# is 3 for our example
# - Use Bash to expand that construct to: $1 $2 $3
# these vars contain the parsed parts of the PATH
# - Finally, display the three parts of the PATH using echo: echo $1 $2 $3
# - This causes the following text to be sent to STDOUT:
# /usr/local/bin /usr/bin /bin
So, can the eval eval... line in the preceding code be simplified, but still produce the desired output, which for the above example is:
/usr/local/bin /usr/bin /bin
I am thinking along the lines of a solution that would replace some of the echo commands with input/output redirection (perhaps) or maybe a reordering/collapsing of sorts that would lead to the need for fewer eval commands than are used in the example.
but still produce the desired output,
/usr/local/bin /usr/bin /bin
Just:
echo "${PATH//:/ }"
The intent is to take the PATH env variable, break it up into its parts, making them
appear to be command line args (i.e., $1, $2, ...), and then for this example, just
echo the parts in space delimited form, but we can imagine that we may want to do other
things with them - this is just sample usage
I do not trust unquoted shell expansions.
IFS=':' read -ra patharr <<<"$PATH"
set -- "${patharr[#]}"
IFS=' '; printf "%s\n" "${patharr[*]}"
echo "${PATH}" | tr ':' '\n' > stack
count=1
echo "#/bin/sh-" | tr '-' '\n' >> stack2
while read line
do
echo "path${count}=${line}" >> stack2
count=$(($count+1))
done < stack
source stack2
Now you've got every section of the path, in its' own named variable.
Sticking close to the original, you can do
IFS=:
set $PATH
echo "$#"
If you don't want to change IFS and PATH, you can do
set $(sed 's/[^=]*=//;s/:/ /g' <<< ${PATH})
echo "$#"
Okay, I've written a shell script to read a file with the format:
shortcut1 /path/to/directory1
shortcut2 /path/to/directory2
and its supposed to read the file and build aliases such that typing shortcut1 cd's me into the mapped directory. The problem is, any of the aliases set in the loop don't persist outside of the script.
Firstly I tried just running the script.
. ./build_shortcuts.sh "~/.shortcuts"
where the file ~/.shortcuts contains
dl ~/Downloads
music /music
dtop ~/Desktop
This didn't work. Then I tried setting some aliases outside of the loop. Such as alias hello='world'; alias world='hellob'. I reran the script, typed alias to get a list of aliases and it did include hello and world as aliases, but not any of those set in the loop.
Then I thought maybe the loop isn't setting them at all, so I added alias as the final command in the script so it would print out the aliases at the end; in this case it did include the aliases but they still didn't persist in my session.
build_shortcuts.sh
script="$(cat $# | sed -r -e 's/#[A-Za-z0-9 ]*$//' -e '/^\s+/s/^\s+//' -e '/^\s*$/d' -)"
# strip comments, entry level indentation & empty lines (in that order) from filestream
echo "${script}" | while read shortcut; do
cut=$(echo "${shortcut}" | awk '{ print $1 }')
dest=$(echo "${shortcut}" | awk '{ $1=nil; print $0 }')
dest="${dest:1}" # trim leading whitespace character
alias "${cut}" &>/dev/null
if [ $? = 0 ]; then
echo "Warning: shortcut \"${cut}\" already exists" >&2
continue # by default, skip overwriting shortcuts
fi
echo alias ${cut}="'cd ${dest}'"
alias "${cut}"="'cd ${dest}'"
done
I want the aliases set in the loop within the script to exist outside of the script. Currently they don't.
I'm running on "GNU bash, version 5.0.7(1)-release (x86_64-pc-linux-gnu)" on arch linux.
From the Bash manual page (the section on Pipelines):
Each command in a pipeline is executed as a separate process (i.e., in a subshell)
Since the loop is done as part of a pipe, it will be a subshell, and the alias command you do in the subshell will only be set for that subshell.
A possible work-around would be to save the aliases to a list, and then perform the actual alias commands in a second loop, a loop which is not part of a pipe or a subshell.
Your script can be reduced a bit: it doesn't need to call out to so many external tools.
while read -r cut dest; do
if alias "${cut}" &>/dev/null; then
echo "Warning: shortcut \"${cut}\" already exists" >&2
else
echo alias ${cut}="'cd ${dest}'"
alias "${cut}"="'cd ${dest}'"
fi
done < <(
sed -E -e 's/#[A-Za-z0-9 ]*$//' -e '/^\s+/s/^\s+//' -e '/^\s*$/d' "$#"
)
after "done", I'm redirecting input from a process substitution: this avoids the "while read" loop from running in a subshell.
#!/bin/sh
BACKUPDIR=$1
for argnum in {2..$#};do
echo ${"$argnum"}
done
I have tried this but it gives me this error:
./backup.sh: 10: ./backup.sh: Bad substitution
Use the shift command to remove $1 from the argument list after you're done reading it (thus renumbering your old $2 to $1, your old $3 to $2, etc):
#!/bin/sh
backupdir=$1; shift
for arg; do
echo "$arg"
done
To provide a literal (but not-particularly-good-practice) equivalent to the code in the question, indirect expansion (absent such security-impacting practices as eval) looks like the following:
#!/bin/bash
# ^^^^-- This IS NOT GUARANTEED TO WORK in /bin/sh
# not idiomatic, not portable to baseline POSIX shells; this is best avoided
backupdir=$1
for ((argnum=2; argnum<=$#; ++argnum)); do
echo "${!argnum}"
done
This question already has answers here:
Bash doesn't parse quotes when converting a string to arguments
(5 answers)
Closed 6 years ago.
Say I have a variable $ARGS which contains the following:
file1.txt "second file.txt" file3.txt
How can I pass the contents of $ARGS as arguments to a command (say cat $ARGS, for example), treating "second file.txt" as one argument and not splitting it into "second and file.txt"?
Ideally, I'd like to be able to pass arguments to any command exactly as they are stored in a variable (read from a text file, but I don't think that's pertinent).
Thanks!
It's possible to do this without either bash arrays or eval: This is one of the few places where the behavior of xargs without either -0 or -d extensions (a behavior which mostly creates bugs) is actually useful.
# this will print each argument on a different line
# ...note that it breaks with arguments containing literal newlines!
xargs printf '%s\n' <<<"$ARGS"
...or...
# this will emit arguments in a NUL-delimited stream
xargs printf '%s\0' <<<"$ARGS"
# in bash 4.4, you can read this into an array like so:
readarray -t -d '' args < <(xargs printf '%s\0' <<<"$ARGS")
yourprog "${args[#]}" # actually run your programs
# in bash 3.x or newer, it's just a bit longer:
args=( );
while IFS= read -r -d '' arg; do
args+=( "$arg" )
done < <(xargs printf '%s\0' <<<"$ARGS")
yourprog "${args[#]}" # actually run your program
# in POSIX sh, you can't safely handle arguments with literal newlines
# ...but, barring that, can do it like this:
set --
while IFS= read -r arg; do
set -- "$#" "$arg"
done < <(printf '%s\n' "$ARGS" | xargs printf '%s\n')
yourprog "$#" # actually run your program
...or, letting xargs itself do the invocation:
# this will call yourprog with ARGS given
# ...but -- beware! -- will cause bugs if there are more arguments than will fit on one
# ...command line invocation.
printf '%s\n' "$ARGS" | xargs yourprog
As mentioned by Jonathan Leffler you can do this with an array.
my_array=( "file1.txt" "second file.txt" "file3.txt" )
cat "${my_array[1]}"
An array's index starts at 0. So if you wanted to cat the first file in your array you would use the index number 0. "${my_array[0]}". If you wanted to run your command on all elements, replace the index number with # or *. For instance instead of "${my_arryay[0]}" you would use "${my_array[#]}"Make sure you quote the array or it will treat any filename with spaces as separate files.
Alternatively if for some reason quoting the array is a problem, you can set IFS (which stands for Internal Field Separator) to equal a newline. If you do this, it's a good idea to save the default IFS to a variable before changing it so you can set it back to the way it was once the script completes. For instance:
# save IFS to a variable
old_IFS=${IFS-$' \t\n'}
#set IFS to a newline
IFS='$\n'
# run your script
my_array=( "file1.txt" "second file.txt" "file3.txt" )
cat ${my_array[1]}
# restore IFS to its default state
IFS=$old_IFS
It's probably better to not mess around with IFS unless you have to. If you can quote the array to make your script work then you should do that.
For a much more in depth look into using arrays see:
http://mywiki.wooledge.org/BashGuide/Arrays
http://wiki.bash-hackers.org/syntax/arrays
http://mywiki.wooledge.org/BashFAQ/005
Without bashisms, plain shell code might need an eval:
# make three temp files and list them.
cd /tmp ; echo ho > ho ; echo ho ho > "ho ho" ; echo ha > ha ;
A='ho "ho ho" ha' ; eval grep -n '.' $A
Output:
ho:1:ho
ho ho:1:ho ho
ha:1:ha
Note that eval is powerful, and if not used responsibly can lead to mischief...
I'm writing a bash script that will
behave differently based on the first argument passed (no prob so far)
pass the rest of the arguments to a command (no prob so far)
behave differently, if an argument contains a string
It looks like:
[[ "${1}" = "-h" || "${1}" = "--help" ]] && echo -e "somehelp"
[[ "${1}" = "echo" ]] && echo ${*:2}
[[ "${1}" = "emerge" ]] && emerge -uDN ${*:2}
some-magic-here
Now, if I do
myscript emerge -a whatever whatever2 --option
it would run
emerge -uDN -a whatever whatever2 --option
But, in case that "whatever" is a string containing *, such as
myscript emerge -uDN -a whatever/* whatever2 --option
I'd want it to run
emerge -uDN -a $(eix -u --only-names whatever/*) whatever2 --option
instead. Any tips?
First, if you are going to pass * to the script, you must prevent expansion by the shell on the command line. The simplest way is to quote it:
myscript emerge -a 'whatever/*' whatever2 --option
Since you are already using the [[ operator, note: the following is a bash only solution and not portable to sh. To determine if $3 contains a * you can use the =~ operator:
[[ "${1}" == "emerge" ]] && {
[[ "$3" =~ "*" ]] && \
emerge -uDN $2 $(eix -u --only-names $3) ${*:4} || \
emerge -uDN ${*:2}
}
You can also rewrite the compound commands into nested if-else statements if the logic gets a bit murky. Give it a try and let me know if you have any issues.
You mention the command line:
myscript emerge -uDN -a whatever/* whatever2 --option
The only ways myscript will see the * in its argument list is if there is no subdirectory whatever under the current directory, or it is an empty directory (strictly: if it contains any files, the names all start with .), and in either case, you don't have shopt -s nullglob set. If those conditions aren't met, the shell invoking myscript will replace the * (in)appropriately and myscript will not see the *. (Of course, if you quote the argument — "whatever/*" or 'whatever/*', then myscript will also see the * metacharacter, regardless of nullglob and the presence or absence of a whatever subdirectory.)
It is not clear whether the * needs to be replaced only when it follows the -a option, or if it should be replaced in any argument whatsoever. I will assume that all arguments need to be replaced; it is not very different if it is only the argument after the -a that should be replaced.
Without the code to handle whatever/*, the command looks like:
[[ "${1}" = "emerge" ]] && exec emerge -uDN "${#:2}" || exit 1
Differences:
The exec is optional but guarantees that nothing after the emerge command will be executed (unless the emerge command can't be found, in which case the || exit 1 ensures nothing else is executed).
Use "$#" to preserve the arguments as presented to your script. Without double quotes, there's no difference between $# and $*. Inside double quotes, "$*" generates a single string, but "$#" generates each argument as passed to the script. The "${#:2}" notation does this for the second up to the last argument.
To handle the * for any argument, we need to detect the *. This is going to be easiest if we use arrays.
The array arglist will contain the arguments to be passed to the emerge command. We need to iterate over the arguments to the script, checking for appearances of *:
arglist=( "-uDN" )
for arg in "${#:2}"
do
case "$arg" in
(*\**) arglist+=( $(eix -u --only-names "$arg") );;
(*) arglist+=( "$arg" );;
esac
done
exec emerge "${arglist[#]}"
exit 1
Note that this assumes that eix will expand metacharacters (* to be precise), rather than relying on the shell to do so. It also assumes, as the question assumes, that there are no spaces in the names generated by eix. If there are any, the $(…) notation will split the names at the spaces (or tabs, or newlines, …).
This code would be best handled as the body of the then clause in
if [[ "${1}" = "emerge" ]]
then
…
fi
This would be clearer than trying to squeeze all that code onto a single line (which could be done, but there is no point in doing so and many points to not doing so).
Here's a clue:
#!/bin/sh
x="bbbccc" # Put different strings in here, and see what happens
case "$x" in
*\**)
echo "Yes"
;;
*)
echo "No"
;;
esac