1

I'm not a bash expert but I've been working on quite a long a bash script lately and it's been getting really bothersome quoting my parameters every single time. Is there any way to change this shell behavior? I would like my strings to not be split even when I don't quote them

P.S - setting IFS to null doesn't do the job because then all arguments gets mangled. what I want is that when writing:

func1() {
    echo $1 $2 $3
}

func2() { echo "number of arguments func2 got: $#" }

func2 $(func1 firstarg "second arg is a multi word string" "third arg is also a multi word string")

would print:

number of arguments func2 got: 3

Edit, thanks for comments for pointing out: since my example is using echo the code would anyway print it got either 1 or 14 arguments depending if you quote the argument to func2 or not. However what I was looking for is a way to return multiple values from a function, without them being mangled, as in the expected result I mentioned

Thanks!

  • IMO what you are saying is essentially that writing the language correctly is too troublesome for you so you want to go out of your way to be able to write it incorrectly. Also your test case almost certainly has no practical purpose and is hardly an issue with word splitting, echo would print that all out the same whether quotes are used or not. – jesse_b Mar 23 '22 at 15:02
  • @jesse_b You're right that I don't understand why this was chosen as the default behavior in the language, I can't see when this would be useful, and having to quote everything reduces readability and is a source of common omission mistakes. regarding what you mentioned about echo, you're right, I didn't necessarily meant using echo, but was asking if there's some way I could pass return values "as is" without needing to worry exactly about this kind of things. I could define a global variable for return values but it looks like it would be a bad practice.. – zombieParrot Mar 23 '22 at 15:17
  • I disagree with the readability part but the way to pass them as is, is to quote them. – jesse_b Mar 23 '22 at 15:21
  • Also word splitting likely exists because the early shells, and even some strictly POSIX shells today don't have arrays, in which case word splitting can be a useful tool when done correctly and intentionally. – jesse_b Mar 23 '22 at 15:26
  • Okay, I guess that makes sense if shells didn't have arrays, thanks – zombieParrot Mar 23 '22 at 15:38
  • 1
    How should func2 know how many arguments func1 got, when func1 just returns a string ? That won't be possible in any language I guess. – pLumo Mar 23 '22 at 15:58
  • You messed up your example. What you really want $1 not to be splitted. Your new example includes an echo destroying any information about former aguments being split or not. – Philippos Mar 23 '22 at 16:45
  • @Philippos That's part of the problem, how are you supposed to return values without echoing them? some other options are returning variable names/file names/using globals. all of them feel hacky. – zombieParrot Mar 23 '22 at 17:56
  • @pLumo The question was if there's a way to make the return values split in the same form they were given as parameters, or have some other sane mechanism to pass information between functions, not if there's a way to make func2 guess how to split one string to many. P.S: if I wanted to do what you said, I could hack it by having func1 choose a separator that doesn't happen in the string, prefixing the string with the separator and returning all arguments delimited by the chosen separator. then parsing it out in func2. but again, super hacky, I want something sane. – zombieParrot Mar 23 '22 at 18:07
  • 1
    @zombieParrot You wrote that you don't want to quote your parameters each time, but in your example quoting would not help at all. – Philippos Mar 24 '22 at 08:57

2 Answers2

5

To answer the question in the title, the way to disable word splitting is to set IFS to the empty string.

But what you described in the text was harder, to keep more than one multi-word string distinct after passing them through a command substitution. And there's really no simple way to do that.

The root issue here is that the result of a command substitution (~ the output to stdout of the function called within it) is just a single string (stream of bytes), and you're trying to fit multiple strings in it. In the general case, with arbitrary contents in those strings, that's a rather fundamental problem.

The also isn't too different from storing a command in a variable, just we have command substitution here instead.


A workaround would be to reserve some character to use as a separator, and set IFS to that. With the default IFS, you'd have space, tab and newline as such separators, which obviously doesn't work for strings with whitespace. With IFS set to the empty string, you'd have no such separator, so you'd always get just one field.

But you could set IFS to e.g. just the newline (assuming your strings aren't multi-line):

#!/bin/bash
IFS=$'\n'
foo() {
    printf "%s\n" "$@"
}

nargs() { echo "number of arguments nargs got: $#" }

nargs $(foo firstarg "multi word string" "also a multi word string")

(You can't use empty strings at the end of the list, though, since the command substitution removes all trailing newlines regardless of IFS.)

Another way is to not use the standard output of a function, but instead pass the name of a variable to the function, and have the function write to the named array via a nameref:

#!/bin/bash
bar() {
    declare -n _out="$1"
    shift
    _out=("$@")
}

nargs() { echo "number of arguments nargs got: $#" }

bar tmparr firstarg "multi word string" "also a multi word string" nargs "${tmparr[@]}"

(There's some issues with variable scoping here if a variable called _out exists outside the function too.)

Also note that if you have unquoted expansions (variables or command substitutions), their results will also be subject to filename globbing, so an output like 12 * 34 would get badly mangled, unless you also disable globbing with set -f.

ilkkachu
  • 138,973
1

What you originally wanted was to disable word splitting after parameter expansion, so instead of writing

mcd() {
  mkdir -p "$1"
  cd "$1"
}

you could write

mcd() {
  mkdir -p $1
  cd $1
}

and it would work with mcd "foo bar" anyhow.

This is the preferred behaviour for most use cases, that's why zsh made it the default behaviour (you can disable it globally with the SH_SPLIT_WORD option or individually by writing ${=1}).

I know you asked for a bash solution, but "use zsh instead of bash" was the answer to so many of my questions, that I really switched to zsh one day and got rid of many problems. Thus, I gladly pass that recommendation to anyone else. IMHO there are few reasons for sticking to bash.

Philippos
  • 13,453
  • Are you missing the IFS="" assignment from the second code block? With the default IFS, calling that function as mcd "foo bar" would create the two directories foo and bar, and probably give an error from cd. Or is there something else you meant here? – ilkkachu Mar 23 '22 at 23:14
  • @ilkkachu Maybe I did not write this clear enough: bash would do as you describe, zsh would create the foo bar folder and enter it by default (without SH_SPLIT_WORD option). – Philippos Mar 24 '22 at 08:14
  • the question title asks for "Any way to disable word splitting in bash?" and it's tagged with [tag:bash], and in that context, the code in your second code block just will not work. I have no idea where zsh came into this. – ilkkachu Mar 24 '22 at 09:36
  • @ilkkachu It's written in my answer, see the last paragraph. I suspect an X-Y-problem. You have my respect for your solution, but if someone asks me how to cut a tree with a knife, my first suggestion is to use a saw instead. Too many people use bash just as a placeholder for shell. – Philippos Mar 24 '22 at 10:29
  • Hiding in the last paragraph the fact that you're suggesting another tool entirely doesn't really help, though. In the context given, the statement "you could write [...] instead and it would work with [...] anyhow" is just untrue. – ilkkachu Mar 24 '22 at 10:31
  • »What you originally wanted was to disable word splitting after parameter expansion, so instead of writing … you could write … and it would work with … anyhow« is true. The OP wanted that it would work anyhow. I'm out of this discussion now. – Philippos Mar 24 '22 at 12:57
  • Well, true in the sense that they could also write sub mcd($) { mkdir($_[0]); chdir($_[0]); }; mcd "foo bar" and it would work. Just that they'd have to switch to Perl first. But in the context the question appears to be asked, the answer as phrased here is just a blatant falsehood. The worst thing is that you know it too, just for some reason you don't seem to want to actually tell your main point, the fact that you switched to another shell, clearly up front. – ilkkachu Mar 24 '22 at 13:20