It's a really good question! It's a little bit complex but hopefully not too much, and it shines a light on some interesting differences between languages.
The answer is roughly that Rust is strongly typed, and println!, format!, etc. all accept arbitrary types. They all also accept a varying number of arguments, and Rust only allows you to define functions with a fixed number of arguments. Python, sh, etc. have neither of these restrictions.
What would the type (function signature) of println/format/etc. be if they were regular functions?
fn println(format: &str, arg1: SomeType?, arg2??);
You could solve the variable-number-of-arguments problem in Rust itself somehow, maybe fn println(format: &str, args...: SomeType);
but you'd have a few problems remaining.First, you'd need to figure out what that type is. The formatting macros don't just accept things that implement Display - they also accept things that implement Debug if you use {:?}, things that implement LowerHex if you use {:x}, etc. And the macro wants to return a compile-time error if you mismatch types and the formatting string, like
println!("Hex {:x}, debug {:?}", [1, 2, 3], 4);
And second, and a little more simply, the macro wants to return a compile-time error if you just have the wrong number of things being formatted: println!("Three things: {} {} {}", 1, 2);
In order to do that level of checking based on the value of the string, it has to be some sort of compile-time extension. In Python, you'd just get a runtime error and in bash, you'd just get empty strings. >>> "{:x}".format("hi")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Unknown format code 'x' for object of type 'str'
>>> "{} {} {}".format(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: tuple index out of range
Regular functions in Rust (and any other language) don't get to see their arguments at compile time; they only see their arguments when they're called.C, of course, just ignores this problem entirely. You don't get an error if you mess up the format string; you just get invalid reads to memory.
printf("Is this a pointer? %s", 1234);
printf("What's the third thing? %d %d %d", 1, 2);
Also (especially) in C, it's very easy to accidentally let the format string be input external to your program: printf(username);
which means that an attacker can specify the format string and cause reads to memory you weren't expecting and cause data to be leaked (or worse, an attacker can cause writes: C's %m token stores the number of bytes written to the next argument to printf, interpreted as a pointer - or the next thing that happens to be memory, if you didn't pass an argument for it). Rust protects against this by making you use a string literal as the first argument.Python also protects against this by having f-strings be a special part of the language syntax. You can't have an f-string that's a variable, so an attacker can't set their username to f"{DATABASE_PASSWORD}". So, in that sense, the f sigil in Python is also a macro, like in Rust - it's just shorter syntax.