Shell Metacharacters

Shell script 에서는 프로그래밍 언어에서처럼 여러가지 연산자를 제공하지 않습니다. 가령 산술연산을 위해서는 별도로 제공되는 산술확장 이나 명령 을 이용하거나 외부 명령을 사용합니다. 하지만 명령문을 작성할 때 사용되는 메타문자들이 있습니다. 이 메타문자들은 명령문 상에서 특별한 기능을 하므로 동일한 문자가 명령문의 스트링에 포함될 경우 반드시 escape 하거나 quote 해야 오류가 발생하지 않습니다.

C++ 소스코드를 생성해주는 템플릿 프로그래밍을 메타프로그래밍 이라고 하죠, 또한 Makefile 을 해당 플랫폼에 맞게 생성해주는 cmake 같은 프로그램을 메타 make 이라고 합니다. regex 에서 사용되는 *, ?, [ ] 와 같은 메타문자나 shell 에서 사용되는 메타문자들은 실제 실행되는 명령문을 생성하는 과정에서 특별한 기능을 수행합니다.

 ( )   `   |   &   ;
 &&  ||                            # AND, OR 문자
 <   >   >>                        # redirection 문자
 *   ?   [ ]                       # glob 문자
 "   '                             # quotes 문자
 \   $      
 =   +=                            # 대입연산

(   )

  • 함수를 작성할 때
  • subshell 을 생성할 때
  • 명령 grouping 에 사용

프로그래밍 언어에서처럼 공백이나 ; 에 대한 제약 없이 자유롭게 사용할 수 있는 메타문자입니다.

`

backtick 문자는 $( ) 와 함께 명령 치환에 사용됩니다.

&&   ||

AND, OR 논리 메타문자.
이것은 shell 메타문자로 [[ ]] 특수 명령에서 제공하는 연산자와는 다른 것입니다.
shell 에서 && || 메타문자는 우선순위를 같게 취급합니다.

&

명령을 background 로 실행할 때 사용합니다.
또한 ; 와 동일하게 명령문의 종료를 나타내므로 명령을 한줄에 연이어 쓸경우 ; 를 붙여서는 안됩니다.

command1 &; command2 &  # error
command1 & command2 &   # OK

{ ... command & ;}      # error
{ ... command & }       # OK

|

명령들을 파이프로 연결할 때 사용합니다.
|& 는 stdout, stderr 둘 다 파이프로 전달하며 2>&1 | 와 동일한 의미입니다.

<   >   >>

이외에도 redirection 에 관련된 여러 메타문자들

>> (append) , >& , <> , &> ( >word 2>&1 ) , &>> ( >>word 2>&1 ) , >| (override noclobber), << (here document) , <<- (no leading tab) , <<< (here string)

;

한 줄에 여러 개의 명령을 연이어 작성할 때 분리를 위해 사용합니다.
;;, ;&, ;;& 는 case 문에서 각 pattern) 의 종료를 나타내는데 사용됩니다.

*   ?   [ ]

패턴매칭에 사용되는 glob 문자도 메타문자에 속합니다.

"   '

문장을 quote 할 때 사용합니다.
space 로 분리된 문장을 quote 하면 하나의 인수가 됩니다.

\

escape 할 때 사용되는 문자입니다.

$

매개변수 확장, 산술 확장, 명령 치환 에 사용됩니다.

=   +=

대입연산에 사용됩니다. += 는 bash 3.1 에서 추가된 기능으로 sh 에서는 사용할 수 없습니다.

메타문자가 명령문에 포함되면 escape 해야한다.

& 메타문자가 들어간 파일 이름을 인수로 사용해 오류 발생

# background process 가 실행되면서 오류발생

$ find * -name foo&bar.txt
[1] 16962
bar.txt: command not found
[1]+  Done  

# 다음과 같이 수정합니다.

$ find * -name foo\&bar.txt

$ find * -name foo"&"bar.txt

$ find * -name foo'&'bar.txt

$ find * -name "foo&bar.txt"

$ find * -name 'foo&bar.txt'

find 명령은 인수로 shell 메타문자인 ( ) , ; 을 사용하는데 quote 하거나 escape 하지 않으면 정상적으로 실행이 되지 않습니다.

# '( )' 는 shell 메타문자 이므로 명령문에 바로 사용할시 오류발생
$ find * -type f ( -name "*.log"  -or  -name "*.bak" ) -exec rm -f {} ;
bash: syntax error near unexpected token '('

# '( )' 는 quote 했으나 ';' 메타문자로 인해 오류발생
$ find * -type f '(' -name "*.log"  -or  -name "*.bak" ')' -exec rm -f {} ;
find: missing argument to '-exec'

# 다음과 같이 shell 메타문자 들을 모두 escape 합니다.
$ find * -type f '(' -name "*.log"  -or  -name "*.bak" ')' -exec rm -f {} ';'
OK

$ find * -type f \( -name "*.log"  -or  -name "*.bak" \) -exec rm -f {} \;
OK

메타문자는 shell 에서 특별히 취급하는 문자다.

그러므로 다른 단어와 공백 없이 붙여서 사용할 수도 있다.

# 메타문자 일경우 '( )'
$ (true)&&(true;false);echo $?
$ 1

# 메타문자가 아닐경우 '{ }'
$ {true;}&&{true;false;};echo $?
bash: syntax error near unexpected token `}'

메타문자는 escape 없이 명령라인 중간에 사용할 수 없다.

$ echo () foo ( ) bar ( zoo )           # 첫 번째 () 는 함수 정의에 해당된다.
bash: syntax error near unexpected token 'foo'

$ echo \(\) foo \( \) bar \( zoo \)     # escape 해야한다.
() foo ( ) bar ( zoo )

$ echo {} foo { } bar { zoo }           # shell 키워드는 OK
{} foo { } bar { zoo }

&& 로 명령들을 연결할 경우

명령들을 && 연산자로 연결하면 이전 명령이 정상적으로 완료된 것을 보장할 수 있습니다. 또한 중간에 하나라도 오류로 종료될 경우 나머지 명령들은 실행되지 않게 됩니다.

다음 명령을 실행하면 현재 디렉토리에 bash 5.1 버전 실행파일이 생성됩니다.

# 명령 실행이 완료되려면 gcc, make 명령과 libtinfo-dev 패키지가 설치되어 있어야 합니다.
$ cd `mktemp -d -p /dev/shm` && 
    ( curl -L https://ftp.gnu.org/gnu/bash/bash-5.1.tar.gz | tar xvz && 
    cd bash-5.1 && ./configure && make -j `nproc` && strip bash ) && 
    mv bash-5.1/bash "$OLDPWD" && rm -rf "$PWD" && cd -

{ } 는 shell keyword

{ } 는 메타문자가 아니고 키워드로 명령 그룹에 사용되며, 이외에도 함수 정의, 매개변수 확장, brace 확장에 사용됩니다.

# 명령 그룹
{ echo 1; echo 2; echo 3 ;}    # 명령 위치에서 사용되므로 shell keyword 

# 함수 정의
f1() { echo 1 ;}

# 매개변수 확장
$AA, ${AA}, ${AA:-0}, ${AA//Linux/Unix} 

# brace 확장
echo file{1..5}
---------------------------

# 다음과 같은 경우는 find 명령의 인수에 해당하는 문자
$ find * -name '*.o' -exec rm -f {} \;

' ! ' shell keyword

! 도 키워드로 쉘에서 두 가지 기능을 가지고 있습니다. 하나는 명령의 위치에서 공백과 함께 사용되면 logical NOT 으로 사용되고, 다른 하나는 프롬프트 상에서 ! 와 공백 없이 붙여서 command history 확장에 사용됩니다.

  • logical NOT
$ [ 1 = 1 ]; echo $?
0
$ ! [ 1 = 1 ]; echo $?    # 명령 위치에서 사용되면 logical NOT 키워드
1

$ if [ 1 = 1 ]; then echo 111; else echo 222; fi
111
$ if ! [ 1 = 1 ]; then echo 111; else echo 222; fi
222
---------------------------------------------

# 다음과 같은 경우는 명령 위치가 아니므로 logical NOT 키워드로 사용된 것이 아니고
# [, test, find, iptables 명령의 인수에 해당하는 문자가 됩니다.

$ [ ! 1 -eq 2 ]

$ test ! -e dir1/foo -o ! -e dir1/bar

$ find dir1 ! -name "*.o"

$ iptables -A WEB_SSH -p tcp ! -s 10.10.10.10 --dport 22 -j DROP
  • command history 확장

    ! 는 프롬프트 상에서 history 확장에 사용되므로 주의해야 합니다.
    ( non-interactive shell 인 스크립트 파일 실행시에는 history 기능이 disable 됩니다 )

$ lsb_release -d
Description:    Ubuntu 15.04

$ echo command history : !lsb       # 이전 명령 history 확장
command history : lsb_release -d

$ echo "hello!lsb world"            # double quotes 에서도 history 확장이 된다!
hellolsb_release -d world   

$ echo 'hello!lsb world'
hello!lsb world

대입 메타문자로 += 를 사용할 수 있다.

+= 대입 메타문자는 변수 와 array 에서 모두 사용할 수 있습니다. ( sh 에서는 사용할 수 없습니다. )

### variable ###         ### indexed array ###       ### associative array ###           
                                                     $ declare -A CC
$ AA=hello               $ BB=(11 22 33)             $ CC=([aa]=111 [bb]=222) 

$ AA+=" world"           $ BB+=(44)                  $ CC+=([cc]=333)

$ echo "$AA"             $ echo ${BB[@]}             $ echo ${CC[@]}
hello world              11 22 33 44                 222 111 333

sh 에서는 다음과 같이 하면 됩니다.

$ AA=hello                $ set 11 22 33

$ AA="$AA world"          $ echo "$@"
                          11 22 33
$ echo "$AA"              
hello world               $ set "$@" 44

                          $ echo "$@"
                          11 22 33 44

Shell 에는 , 연산자가 없다

기본적으로 명령의 인수나 array 원소를 구분하기 위해 공백을 사용합니다.

f1() {                                           $ ARR=(11 , 22 , 33)
    echo arg1 : $1                               
    echo arg2 : $2                               $ echo ${AA[0]} 
    echo arg3 : $3                               11
}                                                $ echo ${AA[1]}   # ',' 가 두번째 
                                                 ,                 # 원소가 된다.
$ f1 11,22,33                                    $ echo ${AA[2]} 
arg1 : 11,22,33                                  22
arg2 :             # 하나의 인수로 전달된다.
arg3 :

$ f1 11, 22, 33
arg1 : 11,         # '11,' 가 첫번째 인수가 된다.
arg2 : 22,
arg3 : 33

따라서 명령문에서 , 를 이용해 옵션 인수를 받을 경우 하나의 인수로 전달되기 위해서 공백을 두어서는 안됩니다.

# ',' 를 공백없이 붙여야 trace= 와함께 하나의 인수로 전달된다.
$ strace -e trace=open,close,read,write command ...    # OK

# 다음과 같이 공백을 두면 안됩니다.
$ strace -e trace=open, close, read, write command ...    # ERROR

$ gcc -shared -Wl,-soname,libmean.so.1 -o libmean.so.1.0.1  ...   # OK

$ iptables -p tcp --tcp-flags ACK,FIN,SYN  ...    # OK

Quiz

C++ 언어에서 매크로 와 템플릿의 확장 결과를 볼 수 있는 함수를 작성하는 것입니다. 인수는 단순히 헤더이름만 줄수도 있고 소스 파일이름을 줄수도 있습니다.

함수 실행을 위해선 clang++, g++, clang-format 명령이 있어야 됩니다.

# 1. 기본적으로 매크로와 템플릿의 확장 결과가 출력됩니다.
# 2. "-m" 옵션을 사용하면 매크로 확장 결과만 출력됩니다.
# 3. "-l" 옵션은 헤더 파일이 참조하는 파일 리스트를 볼 수 있습니다.
# 4. 기타 컴파일러에 전달할 옵션이 있을 경우에는 "--" 이후에 작성하면 됩니다.

$ header-cpp test.cpp                             $ header-cpp <<\@
                                                  template <typename T>
$ cat test.cpp | header-cpp                       T add(T a, T b) { return a + b; }
                                                  int main() {
$ header-cpp -m iostream                              add(100, 200);
                                                      add(1.23, 1.23);
$ header-cpp -m '#include <iostream>'             }
                                                  @
$ echo '#include <vector>' | header-cpp -m
function header-cpp () {
    local opt D='class __________;'
    local command='clang++ -xc++ - -Xclang -ast-print -fsyntax-only -std=c++20'
    for opt; do
        case $opt in
            -m) command='g++ -xc++ - -E -P -dD -std=c++20'; break ;;
            -l) command='g++ -xc++ - -M -std=c++20'; break ;;
        esac
    done
    ( set -o pipefail        # 이후 로는 ( ) 명령 그룹들이 파이프에 연결되므로
        (                    # 각각의 subshell 에서 상속받은 $@ 값을 갖게 됩니다.
            for arg; do 
                case $arg in
                    -m|-l) continue ;;
                    --) break ;;
                esac
                if test -f "$arg"; then
                    cat "$arg"
                else
                    ! [[ $arg =~ ^\#include ]] && echo "#include <$arg>" || echo "$arg"
                fi
            done
            if ! test -t 0; then cat; fi
        ) |
        sed -En -e '/^[ \t]*#[ \t]*include/! s/^/\a/; H;' -e '${ g; tR :R s/\a//g; TX;' \
                -e 's/^[ \t]*#[ \t]*include.*$/\n'"$D"'\n&\n'"$D"'\n/Mg; :X p }' |
        ( while [ $# -gt 0 ]; do [ "$1" = "--" ] && { shift; break; } || shift; done; \
          $command "$@" ) |
        ( [ "$opt" = "-m" ] && clang-format -style="{IndentWidth: 4}" | sed /^"$D"$/d ||
          sed /^"$D"$/,/^"$D"$/d )
    )
}

-----------------------------------------------------------------------

$ cat test.cpp        // 템플릿 확장을 통해 숫자 sequence 를 만드는 예제
#include <iostream>
#include <utility>

template<typename T, T... ints>
void print_sequence(std::integer_sequence<T, ints...> seq)
{
    std::cout << "The sequence of size " << seq.size() << " : ";
    ((std::cout << ints << ' '), ...);     // fold expression 확장 결과도 볼수있다.
    std::cout << '\n';
}

template <typename T, T START, T END, T STEP, T... Indices>
constexpr auto fn_make_sequence(std::integer_sequence<T, Indices...> seq)
{
    if constexpr (STEP > 0 && START < END)   // STEP 이 양수면 오름차순 생성
    {
        return fn_make_sequence<T, START + STEP, END, STEP>
            (std::integer_sequence<T, Indices..., START>{});
    }
    else if constexpr (STEP < 0 && START > END)   // STEP 이 음수면 내림차순 생성
    {
        return fn_make_sequence<T, START + STEP, END, STEP>
            (std::integer_sequence<T, Indices..., START>{});
    }
    else
        return seq;
}

// decltype 으로 감싸면 실제 함수가 실행되지는 않고 함수 반환 타입 정보만 취하게 됩니다.
template <typename T, T START, T END, T STEP>
using make_sequence = decltype(
        fn_make_sequence<T, START, END, STEP>(std::integer_sequence<T>{}));

int main()
{
    // seq 의 타입은 std::integer_sequence<int, 0, 2, 4, 6, 8> 가 된다.
    auto seq = make_sequence<int, 0, 10, 2>{};
    print_sequence(seq);
}

-------------------------------------------------------------

# 다음 명령을 실행하면 test.cpp 파일의 템플릿 확장 결과를 볼 수 있습니다.

$ header-cpp test.cpp

SFINAE (Substitution Failure Is Not An Error) 에 사용되는 enable_if

$ header-cpp <<\@
template <bool, typename T = void> struct enable_if {};
template <typename T> struct enable_if<true, T> { typedef T type; };

int main()
{
    enable_if<true>();
    enable_if<true, int>();
    enable_if<false>();
    enable_if<false, int>();
}
@

template <bool, typename T = void> struct enable_if {
};
template<> struct enable_if<true, void> {              // 인자로 전달된값이 true 면
    typedef void type;                                 // type 이 존재하게 되어
};                                                     // 템플릿 확장이 성공하지만
template<> struct enable_if<true, int> {
    typedef int type;
};
template<> struct enable_if<false, void> {             // 인자로 전달된값이 false 면
};                                                     // type 이 존재하지 않게되어
template<> struct enable_if<false, int> {              // 템플릿 확장이 실패하게 된다.
};                                                     // 이때 컴파일이 중단되는것은 아니고
template <typename T> struct enable_if<true, T> {      // (Is Not An Error) 단지 템플릿
    typedef T type;                                    // 확장 set 에 포함되지 않게 됩니다.
};
int main() {
    enable_if<true>();
    enable_if<true, int>();
    enable_if<false>();
    enable_if<false, int>();
}

오류는 stderr 로 출력되므로 오류만 보고 싶을 경우에는 > /dev/null 와같이 redirect 하면 됩니다. C++20 에 도입된 concept 은 Bjarne Stroustrup 가 오래전부터 주장해왔던 기능이라고 하는데요. concept 을 이용하면 컴파일 타임에 템플릿 확장을 통해 static polymorphism 을 쉽게 구현할 수 있습니다.

$ header-cpp <<\@ > /dev/null
#include <iostream>

template <typename T>
concept has_foo = requires(T t) {      // concept has_foo 는 타입 T 에
    t.foo();                           // foo() 함수가 존재해야 한다.
};

template <has_foo T, has_foo U>        // has_foo 를 타입 T, U 를 constrain 하는데 사용.
void foobar(T f1, U f2) {
    f1.foo();
    f2.foo();
}

struct Bar { void foo() { std::cout << "Bar" << '\n'; } };
struct Zoo { void xxx() { std::cout << "Zoo" << '\n'; } };    // xxx() --> foo() 로 변경

int main() {
    foobar( Bar{}, Zoo{} );
    return 0;
}
@
<stdin>:18:5: error: no matching function for call to 'foobar'
    foobar( Bar{}, Zoo{} );
    ^~~~~~
<stdin>:9:6: note: candidate template ignored: constraints not satisfied [with T = Bar, U = Zoo]
void foobar(T f1, U f2) {
     ^
<stdin>:8:22: note: because 'Zoo' does not satisfy 'has_foo'     <---- has_foo 위반 오류
template <has_foo T, has_foo U>
                     ^
<stdin>:5:7: note: because 't.foo()' would be invalid: no member named 'foo' in 'Zoo'
    t.foo();
      ^
1 error generated.

다음과 같이 concept 를 이용해 operator << 를 오버로딩 하면 std::array, std::vector, std::tuple, std::span, std::pair, std::map, std::set 값들을 모두 출력할 수 있습니다.

#include <iostream>
#include <array>
#include <vector>
#include <deque>
#include <list>
#include <forward_list>
#include <span>
#include <tuple>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <variant>
#include <optional>
#include <memory>

template <template <typename...> class T, typename>
struct is_same_template : std::false_type { };
template <template <typename...> class T, typename... Args>
struct is_same_template<T, T<Args...>> : std::true_type { };
// for std::array, std::span
template <template <typename, auto> class T, typename>
struct is_same_template2 : std::false_type { };
template <template <typename, auto> class T, typename Arg, auto N>
struct is_same_template2<T, T<Arg, N>> : std::true_type { };

template <typename T>
concept array_c = is_same_template2<std::array, std::remove_cvref_t<T>>::value; 
template <typename T>
concept span_c = is_same_template2<std::span, std::remove_cvref_t<T>>::value; 
template <typename T>
concept vector_c = is_same_template<std::vector, std::remove_cvref_t<T>>::value; 
template <typename T>
concept deque_c = is_same_template<std::deque, std::remove_cvref_t<T>>::value; 
template <typename T>
concept list_c = is_same_template<std::list, std::remove_cvref_t<T>>::value; 
template <typename T>
concept forward_list_c = is_same_template<std::forward_list, std::remove_cvref_t<T>>::value; 
template <typename T>
concept pair_c = is_same_template<std::pair, std::remove_cvref_t<T>>::value; 
template <typename T>
concept map_c = is_same_template<std::map, std::remove_cvref_t<T>>::value; 
template <typename T>
concept unordered_map_c = is_same_template<std::unordered_map, std::remove_cvref_t<T>>::value; 
template <typename T>
concept unordered_multimap_c = is_same_template<std::unordered_multimap, std::remove_cvref_t<T>>::value; 
template <typename T>
concept multimap_c = is_same_template<std::multimap, std::remove_cvref_t<T>>::value; 
template <typename T>
concept set_c = is_same_template<std::set, std::remove_cvref_t<T>>::value; 
template <typename T>
concept unordered_set_c = is_same_template<std::unordered_set, std::remove_cvref_t<T>>::value; 
template <typename T>
concept unordered_multiset_c = is_same_template<std::unordered_multiset, std::remove_cvref_t<T>>::value; 
template <typename T>
concept multiset_c = is_same_template<std::multiset, std::remove_cvref_t<T>>::value; 
template <typename T>
concept optional_c = is_same_template<std::optional, std::remove_cvref_t<T>>::value; 
template <typename T>
concept unique_ptr_c = is_same_template<std::unique_ptr, std::remove_cvref_t<T>>::value; 
template <typename T>
concept shared_ptr_c = is_same_template<std::shared_ptr, std::remove_cvref_t<T>>::value; 
template <typename T>
concept weak_ptr_c = is_same_template<std::weak_ptr, std::remove_cvref_t<T>>::value; 
template <typename T>
concept tuple_c = is_same_template<std::tuple, std::remove_cvref_t<T>>::value; 
template <typename T>
concept variant_c = is_same_template<std::variant, std::remove_cvref_t<T>>::value; 

template<typename T>         // 전방선언
requires pair_c<T> || map_c<T> || unordered_map_c<T> || unordered_multimap_c<T> || multimap_c<T>
std::ostream& operator << (std::ostream& os, T&& container);
template<typename T> requires tuple_c<T>
std::ostream& operator << (std::ostream& os, T&& container);
template<typename T> requires variant_c<T> || optional_c<T>
std::ostream& operator << (std::ostream& os, T&& container);
template<typename T>
requires unique_ptr_c<T> || shared_ptr_c<T> || weak_ptr_c<T>
std::ostream& operator << (std::ostream& os, T&& container);
template<typename T, auto N>
requires (! std::same_as<char, std::remove_extent_t<std::remove_cv_t<T>>>)
std::ostream& operator << (std::ostream& os, const T(&array)[N]);

template<typename T>
requires array_c<T> || vector_c<T> || deque_c<T> || list_c<T> || forward_list_c<T> ||
         span_c<T> || set_c<T> || unordered_set_c<T> || unordered_multiset_c<T>  || multiset_c<T>
std::ostream& operator << (std::ostream& os, T&& container) 
{
    if (container.empty())                 // array_c, vector_c, deque_c, list_c ...
        os << "[ ]";                       // 컨셉트를 만족할 경우 다음 코드를
    else {                                 // 이용해 값을 출력합니다.
        auto begin = container.begin();
        auto end = container.end();
        os << '[';
        for (auto it = begin; it != end; )
            os << *it << ( ++it != end ? ", " : "]" );
    }
    return os;
}

template<typename T>
requires pair_c<T> || map_c<T> || unordered_map_c<T> || unordered_multimap_c<T> || multimap_c<T>
std::ostream& operator << (std::ostream& os, T&& container) 
{
    if constexpr (pair_c<T>)
        os << '{' << container.first << " : " << container.second << '}';
    else {
        if (container.empty())                  // pair_c, map_c, unordered_map_c ...
            os << "{ }";                        // 컨셉트를 만족할 경우 다음 코드를
        else {                                  // 이용해 값을 출력합니다.
            auto begin = container.cbegin();
            auto end = container.cend();
            os << '{';
            for (auto it = begin; it != end; )
                os << '{' << it->first << " : " << it->second << '}'
                   << ( ++it != end ? ", " : "}" );
        }
    }
    return os;
}

template<typename T> requires tuple_c<T>      // tuple 값을 출력
std::ostream& operator << (std::ostream& os, T&& container)
{
    std::apply(
        [&os](auto&&... elements) {
            std::size_t n{}, size = std::tuple_size_v<std::remove_cvref_t<T>>;
            os << '[';
            ((os << elements << (++n != size ? ", " : "")), ...);   // fold expression
            os << ']';
        },
        std::forward<T>(container));
    return os;
}

template<typename T>          // variant, optional 값을 출력
requires variant_c<T> || optional_c<T>
std::ostream& operator << (std::ostream& os, T&& container) 
{
    if constexpr (variant_c<T>)
        std::visit( [&os](auto&& v){ os << v; }, std::forward<T>(container));
    else if constexpr (optional_c<T>) {
        if (container.has_value())
            os << container.value();
    }
    return os;
}

template<typename T>          // smart pointer 값을 출력
requires unique_ptr_c<T> || shared_ptr_c<T> || weak_ptr_c<T>
std::ostream& operator << (std::ostream& os, T&& container)
{
    if constexpr(weak_ptr_c<T>) {
        if (container.expired())
            return os;
        else
            return os << *container.lock();
    } else
        return os << *container;
}

template<typename T, auto N>   // C array 값을 출력. char 배열은 제외 (기존 방법으로 출력)
requires (! std::same_as<char, std::remove_extent_t<std::remove_cv_t<T>>>)
std::ostream& operator << (std::ostream& os, const T(&array)[N])
{
    os << '[';
    for (std::size_t i = 0; i < N; )
        os << array[i] << (++i == N ? "" : ", ");
    return os << ']';
}

int main()
{
    using namespace std::literals;
    auto array = std::array { 11, 22, 33 };
    auto vector = std::vector { 44, 55, 66 };
    auto deque = std::deque { 77, 88, 99 };
    auto list = std::list { 77, 88, 99 };
    auto span = std::span { array };
    auto tuple = std::tuple { "hello"s, 3.14, array, vector, list };
    auto pair = std::pair { "foo"s, 100 };
    auto map = std::map { pair, {"bar"s, 200}, {"zoo"s, 300} };
    auto set = std::set { "foo"s, "bar"s, "zoo"s };    // std::string 이어야 정렬이 된다.

    std::cout << "std::array\t" << array << '\n';      // << 연산자로 array, vector, span 
    std::cout << "std::vector\t" << vector << '\n';    // tuple, pair, map, set 값을 모두
    std::cout << "std::deque\t" << deque << '\n';      // 출력할 수 있다.
    std::cout << "std::list\t" << list << '\n';
    std::cout << "std::span\t" << span << '\n';
    std::cout << "std::tuple\t" << tuple << '\n';
    std::cout << "std::pair\t" << pair << '\n';
    std::cout << "std::map\t" << map << '\n';
    std::cout << "std::set\t" << set << '\n';
}

-----------------------------------------------

$ g++ -std=c++20 test.cpp 

$ ./a.out 
std::array      [11, 22, 33]
std::vector     [44, 55, 66]
std::deque      [77, 88, 99]
std::list       [77, 88, 99]
std::span       [11, 22, 33]
std::tuple      [hello, 3.14, [11, 22, 33], [44, 55, 66], [77, 88, 99]]
std::pair       {foo : 100}
std::map        {{bar : 200}, {foo : 100}, {zoo : 300}}
std::set        [bar, foo, zoo]

C++ 템플릿 메타프로그래밍 강좌
https://youtu.be/tiAVWcjIF6o
https://www.youtube.com/user/siliners/videos
https://www.youtube.com/channel/UCgSmRttyuBvl_ng9QxjX0wQ/videos