Shell Script: correct way to declare an empty array - arrays

I'm trying to declare an empty array in Shell Script but I'm experiencing an error.
#!/bin/bash
list=$#
newlist=()
for l in $list; do
newlist+=($l)
done
echo "new"
echo $newlist
When I execute it, I get test.sh: 5: test.sh: Syntax error: "(" unexpected

Run it with bash:
bash test.sh
And seeing the error, it seems you're actually running it with dash:
> dash test.sh
test.sh: 5: test.sh: Syntax error: "(" unexpected
Only this time you probably used the link to it (/bin/sh -> /bin/dash).

I find following syntax more readable.
declare -a <name of array>
For more details see Bash Guide for Beginners: 10.2. Array variables.

In BASH 4+ you can use the following for declaring an empty Array:
declare -a ARRAY_NAME=()
You can then append new items NEW_ITEM1 & NEW_ITEM2 by:
ARRAY_NAME+=(NEW_ITEM1)
ARRAY_NAME+=(NEW_ITEM2)
Please note that parentheses () is required while adding the new items. This is required so that new items are appended as an Array element. If you did miss the (), NEW_ITEM2 will become a String append to first Array Element ARRAY_NAME[0].
Above example will result into:
echo ${ARRAY_NAME[#]}
NEW_ITEM1 NEW_ITEM2
echo ${ARRAY_NAME[0]}
NEW_ITEM1
echo ${ARRAY_NAME[1]}
NEW_ITEM2
Next, if you performed (note the missing parenthesis):
ARRAY_NAME+=NEW_ITEM3
This will result into:
echo ${ARRAY_NAME[#]}
NEW_ITEM1NEW_ITEM3 NEW_ITEM2
echo ${ARRAY_NAME[0]}
NEW_ITEM1NEW_ITEM3
echo ${ARRAY_NAME[1]}
NEW_ITEM2
Thanks to #LenW for correcting me on append operation.

Try this to see if you are oriented to dash or bash
ls -al /bin/sh
If it says /bin/sh -> /bin/dash, then type this:
sudo rm /bin/sh
sudo ln -s /bin/bash /bin/sh
Then type again:
ls -al /bin/sh*
then must says something like this:
/bin/sh -> /bin/bash
It means that now sh is properly oriented to Bash and your arrays will work.

DOMAINS=(1); if [[ ${DOMAINS-} ]]; then # true
unset DOMAINS; if [[ ${DOMAINS-} ]]; then # false

If the array is empty just do this:
NEWLIST=
You can check it with:
if [ $NEWLIST ] ; then
# do something
fi
a non empty array declaration looks like this:
NEWLIST=('1' '2' '3')
To fill an array during process:
ARRAY=("$(find . -name '*.mp3')")
Hope this helps

Related

Declaring BASH. Array of strings in a a script

I'm writing a small script to test my regex understanding of comparison operator "=~". I thought that my syntax was alright but I keep getting:
3: Syntax error: "(" unexpected
this is my small script link to this syntax error :
#!/bin/bash
inputsArr=("ab" "67" "7b7" "g" "67777" "07x7g7" "77777" "7777" "")
for input in ${inputsArr[#]}; do
if [[ "$1" =~ "$input" ]]; then
echo "$?"
fi
done
I try to compare in a loop with an array some "strings" against my arg1 or "$1"
The code works in bash, you just need to run it in the right shell, you can do the following:
bash ./script.sh g
Also type ps -p $$ (not echo $SHELL) to see what shell you are currently in:
Examples:
# ps -p $$
PID TTY TIME CMD
25583 pts/0 00:00:00 sh
# exit
# ps -p $$
PID TTY TIME CMD
22538 pts/0 00:00:00 bash
$SHELL is to tell you what the current user has but you can change on the fly so that is why the other command is more useful.
Borne shell (sh) does not play as nicely with arrays. You have to use eval.
change your default shell to bash. Ref: https://www.tecmint.com/change-a-users-default-shell-in-linux/
I just reach my goal with this !
#!/bin/bash
inputsArr=("ab" "67" "7b7" "g" "67777" "07x7g7" "77777" "7777" "")
for input in ${inputsArr[#]}; do [[ "$input" =~ $1 ]]; echo "$?" ; done
I would like to say thanks you to every person that give me some tips on this basic BASH script problem. Without you I would certainly not reach my goal by my own way and it is beautiful to see this cooperation in action.

Bash: how to print and run a cmd array which has the pipe operator, |, in it

This is a follow-up to my question here: How to write bash function to print and run command when the command has arguments with spaces or things to be expanded
Suppose I have this function to print and run a command stored in an array:
# Print and run the cmd stored in the passed-in array
print_and_run() {
echo "Running cmd: $*"
# run the command by calling all elements of the command array at once
"$#"
}
This works fine:
cmd_array=(ls -a /)
print_and_run "${cmd_array[#]}"
But this does NOT work:
cmd_array=(ls -a / | grep "home")
print_and_run "${cmd_array[#]}"
Error: syntax error near unexpected token `|':
eRCaGuy_hello_world/bash$ ./print_and_run.sh
./print_and_run.sh: line 55: syntax error near unexpected token `|'
./print_and_run.sh: line 55: `cmd_array=(ls -a / | grep "home")'
How can I get this concept to work with the pipe operator (|) in the command?
If you want to treat an array element containing only | as an instruction to generate a pipeline, you can do that. I don't recommend it -- it means you have security risk if you don't verify that variables into your string can't consist only of a single pipe character -- but it's possible.
Below, we create a random single-use "$pipe" sigil to make that attack harder. If you're unwilling to do that, change [[ $arg = "$pipe" ]] to [[ $arg = "|" ]].
# generate something random to make an attacker's job harder
pipe=$(uuidgen)
# use that randomly-generated sigil in place of | in our array
cmd_array=(
ls -a /
"$pipe" grep "home"
)
exec_array_pipe() {
local arg cmd_q
local -a cmd=( )
while (( $# )); do
arg=$1; shift
if [[ $arg = "$pipe" ]]; then
# log an eval-safe copy of what we're about to run
printf -v cmd_q '%q ' "${cmd[#]}"
echo "Starting pipeline component: $cmd_q" >&2
# Recurse into a new copy of ourselves as a child process
"${cmd[#]}" | exec_array_pipe "$#"
return
fi
cmd+=( "$arg" )
done
printf -v cmd_q '%q ' "${cmd[#]}"
echo "Starting pipeline component: $cmd_q" >&2
"${cmd[#]}"
}
exec_array_pipe "${cmd_array[#]}"
See this running in an online sandbox at https://ideone.com/IWOTfO
Do this instead. It works.
print_and_run() {
echo "Running cmd: $1"
eval "$1"
}
Example usage:
cmd='ls -a / | grep -C 9999 --color=always "home"'
print_and_run "$cmd"
Output:
Running cmd: ls -a / | grep -C 9999 --color=always "home"
(rest of output here, with the word "home" highlighted in red)
The general direction is that you don't. You do not store the whole command line to be printed later, and this is not the direction you should take.
The "bad" solution is to use eval.
The "good" solution is to store the literal '|' character inside the array (or some better representation of it) and parse the array, extract the pipe parts and execute them. This is presented by Charles in the other amazing answer. It is just rewriting the parser that already exists in the shell. It requires significant work, and expanding it will require significant work.
The end result is, is that you are reimplementing parts of shell inside shell. Basically writing a shell interpreter in shell. At this point, you can just consider taking Bash sources and implementing a new shopt -o print_the_command_before_executing option in the sources, which might just be simpler.
However, I believe the end goal is to give users a way to see what is being executed. I would propose to approach it like .gitlab-ci.yml does with script: statements. If you want to invent your own language with "debug" support, do just that instead of half-measures. Consider the following YAML file:
- ls -a / | grep "home"
- echo other commands
- for i in "stuff"; do
echo "$i";
done
- |
for i in "stuff"; do
echo "$i"
done
Then the following "runner":
import yaml
import shlex
import os
import sys
script = []
input = yaml.safe_load(open(sys.argv[1], "r"))
for line in input:
script += [
"echo + " + shlex.quote(line).replace("\n", "<newline>"), # some unicode like ␤ would look nice
line,
]
os.execvp("bash", ["bash", "-c", "\n".join(script)])
Executing the runner results in:
+ ls -a / | grep "home"
home
+ echo other commands
other commands
+ for i in "stuff"; do echo "$i"; done
stuff
+ for i in "stuff"; do<newline> echo "$i"<newline>done<newline>
stuff
This offers greater flexibility and is rather simple, supports any shell construct with ease. You can try gitlab-ci/cd on their repository and read the docs.
The YAML format is only an example of the input format. Using special comments like # --- cut --- between parts and extracting each part with the parser will allow running shellcheck over the script. Instead of generating a script with echo statements, you could run Bash interactively, print the part to be executed and then "feed" the part to be executed to interactive Bash. This will alow to preserve $?.
Either way - with a "good" solution, you end up with a custom parser.
Instead of passing an array, you can pass the whole function and use the output of declare -f with some custom parsing:
print_and_run() {
echo "+ $(
declare -f "$1" |
# Remove `f() {` and `}`. Remove indentation.
sed '1d;2d;$d;s/^ *//' |
# Replace newlines with <newline>.
sed -z 's/\n*$//;s/\n/<newline>/'
)"
"$#"
}
cmd() { ls -a / | grep "home"; }
print_and_run cmd
Results in:
+ ls --color -F -a / | grep "home"
home/
It will allow for supporting any shell construct and still allow you to check it with shellcheck and doesn't require that much work.

How to replace an array variable of one shell script from another shell script's array variable?

I have two shell scripts, fruits_original.sh and appending_fruits.sh. In the fruits_original.sh I have one array variable: fruits=('Apple' 'Mango' 'Guava').
What I want to do is I have to write a shell script appending_fruits.sh that will take an argument some new fruits name is Orange and will append that new fruit name to the fruits_original.sh fruits array variable.
After script run fruits array should be remain an array only and its value should be fruits=('Apple' 'Mango' 'Guava' 'Orange').
The file fruits_original.sh has this. Below is the appending_fruits.sh script by this my variable is changing into this fruits= ('Apple' 'Mango' 'Guava' 'Orange'). But when I am trying to do echo "${fruits[#]}" I am getting this error:
line 1: syntax error near unexpected token `('
Any luck ?
fruits= ('Apple' 'Mango' 'Guava')
echo "${fruits[#]}"
declare -a var=$(awk -F'=' '/^fruits=/ {print $2}' fruits_original.sh)
echo "${var[#]}"
var[${#var[#]}]='Orange'
joined=$(printf " '%s'" "${var[#]}")
echo ${joined:1}
echo "${joined[#]}"
sed -i "s/fruits=.*/fruits= ($( echo ${joined:1})) /" fruits_original.sh
Do not modify the script file. Instead, create another file and source the dynamic data from it. I have chosen the location of configuration to be in /tmp directory.
# fruits_original.sh
fruits=()
if [[ -e /tmp/fruits_original.rc ]]; then
. /tmp/fruits_original.rc
fi
some stuff
Then generate the config file. Use declare -p to safely output properly quoted variables.
# appending_fruits.sh
fruits=()
if [[ -e /tmp/fruits_original.rc ]]; then
. /tmp/fruits_original.rc
fi
fruits+=("new fruit")
decalre -p fruits > /tmp/fruits_original.rc
Put a uuid inside fruits_original.sh to recognize where is your snippet that you want to work with.
# fruits_original.sh
# snip 419d0df3-5f08-4511-ad5a-ad24db45aa6c
fruits=()
# snip 419d0df3-5f08-4511-ad5a-ad24db45aa6c
some stuff
Then extract the relevant parts with sed or other tool, declare "$part" it into a variable, append normally and then capture output from declare -p and replace the content between the marks again.
If not going with any of the above and this is only a very toy example to test some stuff, you could:
# read the line from another script
declare "$(sed '/fruits=/!d' fruits_original.sh)"
# append element
fruits+=(Orange)
# create source-able output
new="$(declare -p fruits)"
# remove declare -- in front
new="fruits=${new%*fruits=}"
# Replace the line with declare -p output.
sed -i "s/fruits=.*/fruits=$new/" fruits_original.sh
Notes:
var[${#var[#]}]='Orange' - just var+=(Orange). No need for ${#.
$( echo ${joined:1}) is a useless use of echo (unless you want word splitting and filename expansion).
check your scripts with https://shellcheck.net
fruits= ( is not an assignment and will run a subshell and could cause syntax error. There is no space in assignment around =.
declare -a var=$( - var is not an array (or, it's an array with one element).

Applescript: Using Bash to find missing file from sequenced files

I found here, a code for Bash to be able to find a missing file, and this code works great because I wont be able to know the length of the sequenced files, so this is able to find the missing file without requiring me to input the last number in the sequence.
This is the code:
shopt -s extglob
shopt -s nullglob
arr=( +([0-9]).#(psd) )
for (( i=10#${arr[0]%.*}; i<=10#${arr[-1]%.*}; i++ )); do
printf -v f "%05d" $i;
[[ ! -f "$(echo "$f".*)" ]] && echo "$f is missing"
done
And it works in both terminal and iTerm.
BUT, when used in my Applescript it always reply with file 00000 is missing, when it is not:
set AEPname to "AO-M1P8"
set RENDERfolder to quoted form of POSIX path of "Volumes:RAID STORAGE:CACHES:RENDERS:AE"
set ISAEDONE to do shell script "cd /Volumes/RAID\\ STORAGE/CACHES/RENDERS/AE/AO-M1P8/
#!/bin/bash
shopt -s extglob
shopt -s nullglob
arr=( +([0-9]).#(psd) )
for (( i=10#${arr[0]%.*}; i<=10#${arr[-1]%.*}; i++ )); do
printf -v f \"%05d\" $i;
[[ ! -f \"$(echo \"$f\".*)\" ]] && echo \"$f is missing\"
done
"
display dialog ISAEDONE as text
(*
if ISAEDONE contains "is missing" then
display dialog "FILE IS MISSING"
else
display dialog "ALL FINE"
end if
*)
What I am doing wrong or is there an easier way to accomplish this?
Thanks in advance.
Screenshot
UPDATE
Seems like the way I was doing it, makes the shell unable to get the directory of the files, If I do manually input the directory, seems like it should work, but now I am getting a new kind of error:
sh: line 6: arr: bad array subscript
sh: line 6: arr: bad array subscript
Strange since I don't get this error when manually pasting the code into terminal.
I updated the code.

translation from bash to ash shell: how to handle arrays defined by input?

I try to transfer the excellent example docker-haproxy from centos to alpine.
A shell script is used to process a list of values given as parameters to the script into an array, then write these values plus their index to some file.
The following construction works in bash:
ServerArray=${SERVERS:=$1}
...
for i in ${ServerArray[#]}
do
echo " " server SERVER_$COUNT $i >> /haproxy/haproxy.cfg
let "COUNT += 1"
done
but not in ash (or sh):
syntax error: bad substitution
The error refers to line
for i in ${ServerArray[#]}
What is the correct syntax here? I guess the line
ServerArray=${SERVERS:=$1}
does not define an array as intended, but googling for long did not help me.
bash to sh (ash) spoofing says
sh apparently has no arrays.
If so, how to solve the problem then?
I guess I can do with this construction:
#!/bin/sh
# test.sh
while [ $# -gt 0 ]
do
echo $1
shift
done
delivers
/ # ./test 172.17.0.2:3306 172.17.0.3:3306
172.17.0.2:3306
172.17.0.3:3306
which is what I need to proceed

Resources