Although it's not necessary for what you are trying to do (just pass multiple commands to a single sed process), it is possible to build pipelines of commands dynamically in Bash.
This Shellcheck-clean Bash code demonstrates one way to do it:
#! /bin/bash -p function run_sed_pipeline { local pipecmd='' i for ((i=1; i<=$#; i++)); do pipecmd+="${pipecmd:+ | }sed \"s/\${$i}/$i/\"" done printf 'DEBUG: EVAL: %s\n' "$pipecmd" >&2 eval "$pipecmd" } run_sed_pipeline A B C D <example.txt
When the code is run it produces output:
DEBUG: EVAL: sed "s/${1}/1/" | sed "s/${2}/2/" | sed "s/${3}/3/" | sed "s/${4}/4/" 1 2 3 4
- The basic idea is to build up a pipeline of commands in a string variable and use
eval to run it. - There are serious pitfalls associated with
eval and it is best avoided. See Why should eval be avoided in Bash, and what should I use instead?. Also see BashFAQ/048 (Eval command and security issues). - I think that the code here avoids significant
eval pitfalls, but I could be wrong. The main thing that the code does to avoid problems is to refrain from putting the expanded function arguments (A B C D in this example) in the string to be evaled. Instead, the only expansions in the command string are (quoted) ${1}, ${2}, ${3}, and ${4}. This ensures that embedded expansions, or quotes etc., in the function arguments will not cause problems.
Another way to create a dynamic pipeline of commands is to use a recursive function, as with this Shellcheck-clean code:
#! /bin/bash -p function run_sed_pipeline { if (( $# < 2 )); then cat else sed "s/$2/$1/" | run_sed_pipeline "$(($1+1))" "${@:3}" fi } run_sed_pipeline 1 A B C D <example.txt
When the code is run it produces output:
1 2 3 4
- The first argument to the function is the number to substitute for the first (remaining) argument to be replaced. Each recursive call increments the first argument by one and removes the first of the following (remaining) arguments.
- This code features an unusual "useless use of cat" (UUoC). The last process in the pipeline is a useless
cat. It's easy to avoid, but doing so makes the code a bit more complicated so I left it as it is for this (illustrative) example.
cat. Usingcat fileresults in the component to the right of it in a pipeline getting a FIFO that it can only read once, front-to-back. For some programs this forces them to be very inefficient compared to getting a real file descriptor -- for example, if you givesorta real FD it can split up into threads and have each one process a piece of the file in parallel; if you givetaila real FD it can skip straight to the end no matter how long it is, etc