#!/usr/bin/python3
'''
Hatless Folded Text: Lib, Utility
Copyright (C) 2019 M. Zhou <lumin@debian.org>
'''
import argparse
import sys, os, re, stat
from typing import *

__VERSION__ = '0.0d'

__HFT_SPEC__ = '''
Hatless Folded Text (HFT) Specification
---------------------------------------

Hatless Folded Text basically resembles sort of plaintext 'tar'.
An HFT file, is a series of (filepath,content) plain texts,
where the filepath line has a special format "^ FILENAME [mode]".
For example, "^ debian/rules 0o755", "^ /tmp/foobar".
The content can contain any plain text except for any line
starts with the hat (^). Comment lines starts with "^#".

HFT spec has some features:

  1. It doesn't support embedding binary blobs, deliberately.
  2. The content must NOT contain any line that starts with a hat (^).
  3. The file mode is optional.

HFT Example
-----------

::
    ^# This is a comment
    ^ debian/compat
    11
    ^ debian/source/format
    3.0 (native)
    ^ debian/rules 0o755
    #!/usr/bin/make -f
    %:
        dh $@
    ^ debian/README
    first line of readme
    ^# You can even write a comment in-line
    second line of readme
'''


def hft_fold(path: str, *, verbose=False) -> List[str]:
    '''
    Fold a given file or directory into hft format. It is basically
    equivalent to::

        #!/bin/sh
        for file in $(find $1 -type f | sort); do
            echo "^ $file"
            cat $file
        done
    '''
    if os.path.isfile(path):
        raise NotImplementedError
    elif os.path.isdir(path):
        hft = []
        for root, dirs, files in os.walk(path):
            for name in files:
                p = os.path.join(root, name)
                if not os.path.isfile(p):
                    raise TypeError(f"Unsupported File Type: {p}!")
                fmode = oct(os.stat(p).st_mode)
                hft.append(f'^ {p} {fmode}\n')
                with open(p, 'r') as f:
                    if verbose: print(p)
                    lines = f.readlines()
                    if any([x.startswith('^') for x in lines]):
                        raise SyntaxError("Hat(^)-leading line is not allowed!")
                    hft.extend(lines)
        return hft
    else:
        raise TypeError(f"Unsupported {path}!")


def hft_unfold(hft: List[str], destdir='', *,
        fp=None, verbose=False, force=False) -> None:
    '''
    Unfold an HFT file.
    '''
    if len(hft) == 0:  # Recursion boundary
        if fp is not None: fp.close()
        return
    else:
        curline, restlines = hft[0], hft[1:]
        if re.match('^\^#.*$', curline):
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        elif curline.startswith('^'):
            if fp is not None: fp.close()
            # parse the "^ filepath [mode]" line
            fmode = None
            if re.match('^\^\s*([/\S]+)\s+([o\d]+)\s*$', curline):
                path, fmode = re.match('^\^\s*([/\S]+)\s+([o\d]+)\s*$',
                        curline).groups()
                fmode = eval(fmode)
            elif re.match('^\^\s*([/\S]+)\s*$', curline):
                path = re.match('^\^\s*([/\S]+)\s*$', curline).group(1)
            else:
                raise SyntaxError(f"Cannot parse '{curline}' !")
            # create the new file
            path = os.path.join(destdir, path)
            if os.path.exists(path) and not force:
                raise Exception(f'{path} already exists!')
            if not os.path.exists(os.path.dirname(path)):
                os.makedirs(os.path.dirname(path))
            fp = open(path, 'w')
            if fmode is not None: os.chmod(path, fmode)
            if verbose: print(path)
            # proceed with the rest lines
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        else:
            if fp is None:
                raise SyntaxError(f'Doesnt know where to write {hft}')
            fp.write(curline)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)


if __name__ == '__main__':

    ag = argparse.ArgumentParser()
    ag.add_argument('-f', '--fold', type=str, default='', required=False,
            help='fold the given file or directory')
    ag.add_argument('-o', '--output', type=str, default='', required=False,
            help='specify output file of the folded result')
    ag.add_argument('-u', '--unfold', type=str, default='', required=False,
            help='unfold the given HFT file')
    ag.add_argument('-d', '--destdir', type=str, default='', required=False,
            help='destination directory when unfolding an HFT file')
    ag.add_argument('-F', '--force', action='store_true', required=False,
            help='force overwrite the output destination')
    ag.add_argument('-S', '--spec', action='store_true', required=False,
            help='print the Hatless Folded Text spec')
    ag.add_argument('-v', '--verbose', action='store_true', required=False,
            help='toggle verbose mode')
    ag.add_argument('-V', '--version', action='store_true', required=False,
            help='print version information')
    ag, agparser = ag.parse_args(), ag

    # Misc info.
    if ag.version:
        print(f'Hatless Folded Text, Version {__VERSION__}')
        exit(0)
    elif ag.spec:
        print(__HFT_SPEC__)
        exit(0)

    # Do folding / unfolding
    if not any([ag.fold, ag.unfold]):
        agparser.print_help()
    elif all([ag.fold, ag.unfold]):
        raise ValueError("Both fold and unfold have been specified!")
    elif ag.fold:
        if not ag.output:
            ag.output = re.sub('/*$', '', ag.fold) + '.hft'
        if os.path.exists(ag.output) and not ag.force:
            raise Exception(f"{ag.output} already exists!")
        hft = hft_fold(ag.fold, verbose=ag.verbose)
        with open(ag.output, 'w') as f:
            f.writelines(hft)
    elif ag.unfold:
        with open(ag.unfold, 'r') as f:
            hft = f.readlines()
        hft_unfold(hft, ag.destdir, verbose=ag.verbose, force=ag.force)

fe0873dce7cd22ced973298bdb0677048b3a0b28
<mask token>


def hft_fold(path: str, *, verbose=False) ->List[str]:
    """
    Fold a given file or directory into hft format. It is basically
    equivalent to::

        #!/bin/sh
        for file in $(find $1 -type f | sort); do
            echo "^ $file"
            cat $file
        done
    """
    if os.path.isfile(path):
        raise NotImplementedError
    elif os.path.isdir(path):
        hft = []
        for root, dirs, files in os.walk(path):
            for name in files:
                p = os.path.join(root, name)
                if not os.path.isfile(p):
                    raise TypeError(f'Unsupported File Type: {p}!')
                fmode = oct(os.stat(p).st_mode)
                hft.append(f'^ {p} {fmode}\n')
                with open(p, 'r') as f:
                    if verbose:
                        print(p)
                    lines = f.readlines()
                    if any([x.startswith('^') for x in lines]):
                        raise SyntaxError('Hat(^)-leading line is not allowed!'
                            )
                    hft.extend(lines)
        return hft
    else:
        raise TypeError(f'Unsupported {path}!')


def hft_unfold(hft: List[str], destdir='', *, fp=None, verbose=False, force
    =False) ->None:
    """
    Unfold an HFT file.
    """
    if len(hft) == 0:
        if fp is not None:
            fp.close()
        return
    else:
        curline, restlines = hft[0], hft[1:]
        if re.match('^\\^#.*$', curline):
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        elif curline.startswith('^'):
            if fp is not None:
                fp.close()
            fmode = None
            if re.match('^\\^\\s*([/\\S]+)\\s+([o\\d]+)\\s*$', curline):
                path, fmode = re.match('^\\^\\s*([/\\S]+)\\s+([o\\d]+)\\s*$',
                    curline).groups()
                fmode = eval(fmode)
            elif re.match('^\\^\\s*([/\\S]+)\\s*$', curline):
                path = re.match('^\\^\\s*([/\\S]+)\\s*$', curline).group(1)
            else:
                raise SyntaxError(f"Cannot parse '{curline}' !")
            path = os.path.join(destdir, path)
            if os.path.exists(path) and not force:
                raise Exception(f'{path} already exists!')
            if not os.path.exists(os.path.dirname(path)):
                os.makedirs(os.path.dirname(path))
            fp = open(path, 'w')
            if fmode is not None:
                os.chmod(path, fmode)
            if verbose:
                print(path)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        else:
            if fp is None:
                raise SyntaxError(f'Doesnt know where to write {hft}')
            fp.write(curline)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)


<mask token>

<mask token>


def hft_fold(path: str, *, verbose=False) ->List[str]:
    """
    Fold a given file or directory into hft format. It is basically
    equivalent to::

        #!/bin/sh
        for file in $(find $1 -type f | sort); do
            echo "^ $file"
            cat $file
        done
    """
    if os.path.isfile(path):
        raise NotImplementedError
    elif os.path.isdir(path):
        hft = []
        for root, dirs, files in os.walk(path):
            for name in files:
                p = os.path.join(root, name)
                if not os.path.isfile(p):
                    raise TypeError(f'Unsupported File Type: {p}!')
                fmode = oct(os.stat(p).st_mode)
                hft.append(f'^ {p} {fmode}\n')
                with open(p, 'r') as f:
                    if verbose:
                        print(p)
                    lines = f.readlines()
                    if any([x.startswith('^') for x in lines]):
                        raise SyntaxError('Hat(^)-leading line is not allowed!'
                            )
                    hft.extend(lines)
        return hft
    else:
        raise TypeError(f'Unsupported {path}!')


def hft_unfold(hft: List[str], destdir='', *, fp=None, verbose=False, force
    =False) ->None:
    """
    Unfold an HFT file.
    """
    if len(hft) == 0:
        if fp is not None:
            fp.close()
        return
    else:
        curline, restlines = hft[0], hft[1:]
        if re.match('^\\^#.*$', curline):
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        elif curline.startswith('^'):
            if fp is not None:
                fp.close()
            fmode = None
            if re.match('^\\^\\s*([/\\S]+)\\s+([o\\d]+)\\s*$', curline):
                path, fmode = re.match('^\\^\\s*([/\\S]+)\\s+([o\\d]+)\\s*$',
                    curline).groups()
                fmode = eval(fmode)
            elif re.match('^\\^\\s*([/\\S]+)\\s*$', curline):
                path = re.match('^\\^\\s*([/\\S]+)\\s*$', curline).group(1)
            else:
                raise SyntaxError(f"Cannot parse '{curline}' !")
            path = os.path.join(destdir, path)
            if os.path.exists(path) and not force:
                raise Exception(f'{path} already exists!')
            if not os.path.exists(os.path.dirname(path)):
                os.makedirs(os.path.dirname(path))
            fp = open(path, 'w')
            if fmode is not None:
                os.chmod(path, fmode)
            if verbose:
                print(path)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        else:
            if fp is None:
                raise SyntaxError(f'Doesnt know where to write {hft}')
            fp.write(curline)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)


if __name__ == '__main__':
    ag = argparse.ArgumentParser()
    ag.add_argument('-f', '--fold', type=str, default='', required=False,
        help='fold the given file or directory')
    ag.add_argument('-o', '--output', type=str, default='', required=False,
        help='specify output file of the folded result')
    ag.add_argument('-u', '--unfold', type=str, default='', required=False,
        help='unfold the given HFT file')
    ag.add_argument('-d', '--destdir', type=str, default='', required=False,
        help='destination directory when unfolding an HFT file')
    ag.add_argument('-F', '--force', action='store_true', required=False,
        help='force overwrite the output destination')
    ag.add_argument('-S', '--spec', action='store_true', required=False,
        help='print the Hatless Folded Text spec')
    ag.add_argument('-v', '--verbose', action='store_true', required=False,
        help='toggle verbose mode')
    ag.add_argument('-V', '--version', action='store_true', required=False,
        help='print version information')
    ag, agparser = ag.parse_args(), ag
    if ag.version:
        print(f'Hatless Folded Text, Version {__VERSION__}')
        exit(0)
    elif ag.spec:
        print(__HFT_SPEC__)
        exit(0)
    if not any([ag.fold, ag.unfold]):
        agparser.print_help()
    elif all([ag.fold, ag.unfold]):
        raise ValueError('Both fold and unfold have been specified!')
    elif ag.fold:
        if not ag.output:
            ag.output = re.sub('/*$', '', ag.fold) + '.hft'
        if os.path.exists(ag.output) and not ag.force:
            raise Exception(f'{ag.output} already exists!')
        hft = hft_fold(ag.fold, verbose=ag.verbose)
        with open(ag.output, 'w') as f:
            f.writelines(hft)
    elif ag.unfold:
        with open(ag.unfold, 'r') as f:
            hft = f.readlines()
        hft_unfold(hft, ag.destdir, verbose=ag.verbose, force=ag.force)

<mask token>
__VERSION__ = '0.0d'
__HFT_SPEC__ = """
Hatless Folded Text (HFT) Specification
---------------------------------------

Hatless Folded Text basically resembles sort of plaintext 'tar'.
An HFT file, is a series of (filepath,content) plain texts,
where the filepath line has a special format "^ FILENAME [mode]".
For example, "^ debian/rules 0o755", "^ /tmp/foobar".
The content can contain any plain text except for any line
starts with the hat (^). Comment lines starts with "^#".

HFT spec has some features:

  1. It doesn't support embedding binary blobs, deliberately.
  2. The content must NOT contain any line that starts with a hat (^).
  3. The file mode is optional.

HFT Example
-----------

::
    ^# This is a comment
    ^ debian/compat
    11
    ^ debian/source/format
    3.0 (native)
    ^ debian/rules 0o755
    #!/usr/bin/make -f
    %:
        dh $@
    ^ debian/README
    first line of readme
    ^# You can even write a comment in-line
    second line of readme
"""


def hft_fold(path: str, *, verbose=False) ->List[str]:
    """
    Fold a given file or directory into hft format. It is basically
    equivalent to::

        #!/bin/sh
        for file in $(find $1 -type f | sort); do
            echo "^ $file"
            cat $file
        done
    """
    if os.path.isfile(path):
        raise NotImplementedError
    elif os.path.isdir(path):
        hft = []
        for root, dirs, files in os.walk(path):
            for name in files:
                p = os.path.join(root, name)
                if not os.path.isfile(p):
                    raise TypeError(f'Unsupported File Type: {p}!')
                fmode = oct(os.stat(p).st_mode)
                hft.append(f'^ {p} {fmode}\n')
                with open(p, 'r') as f:
                    if verbose:
                        print(p)
                    lines = f.readlines()
                    if any([x.startswith('^') for x in lines]):
                        raise SyntaxError('Hat(^)-leading line is not allowed!'
                            )
                    hft.extend(lines)
        return hft
    else:
        raise TypeError(f'Unsupported {path}!')


def hft_unfold(hft: List[str], destdir='', *, fp=None, verbose=False, force
    =False) ->None:
    """
    Unfold an HFT file.
    """
    if len(hft) == 0:
        if fp is not None:
            fp.close()
        return
    else:
        curline, restlines = hft[0], hft[1:]
        if re.match('^\\^#.*$', curline):
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        elif curline.startswith('^'):
            if fp is not None:
                fp.close()
            fmode = None
            if re.match('^\\^\\s*([/\\S]+)\\s+([o\\d]+)\\s*$', curline):
                path, fmode = re.match('^\\^\\s*([/\\S]+)\\s+([o\\d]+)\\s*$',
                    curline).groups()
                fmode = eval(fmode)
            elif re.match('^\\^\\s*([/\\S]+)\\s*$', curline):
                path = re.match('^\\^\\s*([/\\S]+)\\s*$', curline).group(1)
            else:
                raise SyntaxError(f"Cannot parse '{curline}' !")
            path = os.path.join(destdir, path)
            if os.path.exists(path) and not force:
                raise Exception(f'{path} already exists!')
            if not os.path.exists(os.path.dirname(path)):
                os.makedirs(os.path.dirname(path))
            fp = open(path, 'w')
            if fmode is not None:
                os.chmod(path, fmode)
            if verbose:
                print(path)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        else:
            if fp is None:
                raise SyntaxError(f'Doesnt know where to write {hft}')
            fp.write(curline)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)


if __name__ == '__main__':
    ag = argparse.ArgumentParser()
    ag.add_argument('-f', '--fold', type=str, default='', required=False,
        help='fold the given file or directory')
    ag.add_argument('-o', '--output', type=str, default='', required=False,
        help='specify output file of the folded result')
    ag.add_argument('-u', '--unfold', type=str, default='', required=False,
        help='unfold the given HFT file')
    ag.add_argument('-d', '--destdir', type=str, default='', required=False,
        help='destination directory when unfolding an HFT file')
    ag.add_argument('-F', '--force', action='store_true', required=False,
        help='force overwrite the output destination')
    ag.add_argument('-S', '--spec', action='store_true', required=False,
        help='print the Hatless Folded Text spec')
    ag.add_argument('-v', '--verbose', action='store_true', required=False,
        help='toggle verbose mode')
    ag.add_argument('-V', '--version', action='store_true', required=False,
        help='print version information')
    ag, agparser = ag.parse_args(), ag
    if ag.version:
        print(f'Hatless Folded Text, Version {__VERSION__}')
        exit(0)
    elif ag.spec:
        print(__HFT_SPEC__)
        exit(0)
    if not any([ag.fold, ag.unfold]):
        agparser.print_help()
    elif all([ag.fold, ag.unfold]):
        raise ValueError('Both fold and unfold have been specified!')
    elif ag.fold:
        if not ag.output:
            ag.output = re.sub('/*$', '', ag.fold) + '.hft'
        if os.path.exists(ag.output) and not ag.force:
            raise Exception(f'{ag.output} already exists!')
        hft = hft_fold(ag.fold, verbose=ag.verbose)
        with open(ag.output, 'w') as f:
            f.writelines(hft)
    elif ag.unfold:
        with open(ag.unfold, 'r') as f:
            hft = f.readlines()
        hft_unfold(hft, ag.destdir, verbose=ag.verbose, force=ag.force)

<mask token>
import argparse
import sys, os, re, stat
from typing import *
__VERSION__ = '0.0d'
__HFT_SPEC__ = """
Hatless Folded Text (HFT) Specification
---------------------------------------

Hatless Folded Text basically resembles sort of plaintext 'tar'.
An HFT file, is a series of (filepath,content) plain texts,
where the filepath line has a special format "^ FILENAME [mode]".
For example, "^ debian/rules 0o755", "^ /tmp/foobar".
The content can contain any plain text except for any line
starts with the hat (^). Comment lines starts with "^#".

HFT spec has some features:

  1. It doesn't support embedding binary blobs, deliberately.
  2. The content must NOT contain any line that starts with a hat (^).
  3. The file mode is optional.

HFT Example
-----------

::
    ^# This is a comment
    ^ debian/compat
    11
    ^ debian/source/format
    3.0 (native)
    ^ debian/rules 0o755
    #!/usr/bin/make -f
    %:
        dh $@
    ^ debian/README
    first line of readme
    ^# You can even write a comment in-line
    second line of readme
"""


def hft_fold(path: str, *, verbose=False) ->List[str]:
    """
    Fold a given file or directory into hft format. It is basically
    equivalent to::

        #!/bin/sh
        for file in $(find $1 -type f | sort); do
            echo "^ $file"
            cat $file
        done
    """
    if os.path.isfile(path):
        raise NotImplementedError
    elif os.path.isdir(path):
        hft = []
        for root, dirs, files in os.walk(path):
            for name in files:
                p = os.path.join(root, name)
                if not os.path.isfile(p):
                    raise TypeError(f'Unsupported File Type: {p}!')
                fmode = oct(os.stat(p).st_mode)
                hft.append(f'^ {p} {fmode}\n')
                with open(p, 'r') as f:
                    if verbose:
                        print(p)
                    lines = f.readlines()
                    if any([x.startswith('^') for x in lines]):
                        raise SyntaxError('Hat(^)-leading line is not allowed!'
                            )
                    hft.extend(lines)
        return hft
    else:
        raise TypeError(f'Unsupported {path}!')


def hft_unfold(hft: List[str], destdir='', *, fp=None, verbose=False, force
    =False) ->None:
    """
    Unfold an HFT file.
    """
    if len(hft) == 0:
        if fp is not None:
            fp.close()
        return
    else:
        curline, restlines = hft[0], hft[1:]
        if re.match('^\\^#.*$', curline):
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        elif curline.startswith('^'):
            if fp is not None:
                fp.close()
            fmode = None
            if re.match('^\\^\\s*([/\\S]+)\\s+([o\\d]+)\\s*$', curline):
                path, fmode = re.match('^\\^\\s*([/\\S]+)\\s+([o\\d]+)\\s*$',
                    curline).groups()
                fmode = eval(fmode)
            elif re.match('^\\^\\s*([/\\S]+)\\s*$', curline):
                path = re.match('^\\^\\s*([/\\S]+)\\s*$', curline).group(1)
            else:
                raise SyntaxError(f"Cannot parse '{curline}' !")
            path = os.path.join(destdir, path)
            if os.path.exists(path) and not force:
                raise Exception(f'{path} already exists!')
            if not os.path.exists(os.path.dirname(path)):
                os.makedirs(os.path.dirname(path))
            fp = open(path, 'w')
            if fmode is not None:
                os.chmod(path, fmode)
            if verbose:
                print(path)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        else:
            if fp is None:
                raise SyntaxError(f'Doesnt know where to write {hft}')
            fp.write(curline)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)


if __name__ == '__main__':
    ag = argparse.ArgumentParser()
    ag.add_argument('-f', '--fold', type=str, default='', required=False,
        help='fold the given file or directory')
    ag.add_argument('-o', '--output', type=str, default='', required=False,
        help='specify output file of the folded result')
    ag.add_argument('-u', '--unfold', type=str, default='', required=False,
        help='unfold the given HFT file')
    ag.add_argument('-d', '--destdir', type=str, default='', required=False,
        help='destination directory when unfolding an HFT file')
    ag.add_argument('-F', '--force', action='store_true', required=False,
        help='force overwrite the output destination')
    ag.add_argument('-S', '--spec', action='store_true', required=False,
        help='print the Hatless Folded Text spec')
    ag.add_argument('-v', '--verbose', action='store_true', required=False,
        help='toggle verbose mode')
    ag.add_argument('-V', '--version', action='store_true', required=False,
        help='print version information')
    ag, agparser = ag.parse_args(), ag
    if ag.version:
        print(f'Hatless Folded Text, Version {__VERSION__}')
        exit(0)
    elif ag.spec:
        print(__HFT_SPEC__)
        exit(0)
    if not any([ag.fold, ag.unfold]):
        agparser.print_help()
    elif all([ag.fold, ag.unfold]):
        raise ValueError('Both fold and unfold have been specified!')
    elif ag.fold:
        if not ag.output:
            ag.output = re.sub('/*$', '', ag.fold) + '.hft'
        if os.path.exists(ag.output) and not ag.force:
            raise Exception(f'{ag.output} already exists!')
        hft = hft_fold(ag.fold, verbose=ag.verbose)
        with open(ag.output, 'w') as f:
            f.writelines(hft)
    elif ag.unfold:
        with open(ag.unfold, 'r') as f:
            hft = f.readlines()
        hft_unfold(hft, ag.destdir, verbose=ag.verbose, force=ag.force)

#!/usr/bin/python3
'''
Hatless Folded Text: Lib, Utility
Copyright (C) 2019 M. Zhou <lumin@debian.org>
'''
import argparse
import sys, os, re, stat
from typing import *

__VERSION__ = '0.0d'

__HFT_SPEC__ = '''
Hatless Folded Text (HFT) Specification
---------------------------------------

Hatless Folded Text basically resembles sort of plaintext 'tar'.
An HFT file, is a series of (filepath,content) plain texts,
where the filepath line has a special format "^ FILENAME [mode]".
For example, "^ debian/rules 0o755", "^ /tmp/foobar".
The content can contain any plain text except for any line
starts with the hat (^). Comment lines starts with "^#".

HFT spec has some features:

  1. It doesn't support embedding binary blobs, deliberately.
  2. The content must NOT contain any line that starts with a hat (^).
  3. The file mode is optional.

HFT Example
-----------

::
    ^# This is a comment
    ^ debian/compat
    11
    ^ debian/source/format
    3.0 (native)
    ^ debian/rules 0o755
    #!/usr/bin/make -f
    %:
        dh $@
    ^ debian/README
    first line of readme
    ^# You can even write a comment in-line
    second line of readme
'''


def hft_fold(path: str, *, verbose=False) -> List[str]:
    '''
    Fold a given file or directory into hft format. It is basically
    equivalent to::

        #!/bin/sh
        for file in $(find $1 -type f | sort); do
            echo "^ $file"
            cat $file
        done
    '''
    if os.path.isfile(path):
        raise NotImplementedError
    elif os.path.isdir(path):
        hft = []
        for root, dirs, files in os.walk(path):
            for name in files:
                p = os.path.join(root, name)
                if not os.path.isfile(p):
                    raise TypeError(f"Unsupported File Type: {p}!")
                fmode = oct(os.stat(p).st_mode)
                hft.append(f'^ {p} {fmode}\n')
                with open(p, 'r') as f:
                    if verbose: print(p)
                    lines = f.readlines()
                    if any([x.startswith('^') for x in lines]):
                        raise SyntaxError("Hat(^)-leading line is not allowed!")
                    hft.extend(lines)
        return hft
    else:
        raise TypeError(f"Unsupported {path}!")


def hft_unfold(hft: List[str], destdir='', *,
        fp=None, verbose=False, force=False) -> None:
    '''
    Unfold an HFT file.
    '''
    if len(hft) == 0:  # Recursion boundary
        if fp is not None: fp.close()
        return
    else:
        curline, restlines = hft[0], hft[1:]
        if re.match('^\^#.*$', curline):
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        elif curline.startswith('^'):
            if fp is not None: fp.close()
            # parse the "^ filepath [mode]" line
            fmode = None
            if re.match('^\^\s*([/\S]+)\s+([o\d]+)\s*$', curline):
                path, fmode = re.match('^\^\s*([/\S]+)\s+([o\d]+)\s*$',
                        curline).groups()
                fmode = eval(fmode)
            elif re.match('^\^\s*([/\S]+)\s*$', curline):
                path = re.match('^\^\s*([/\S]+)\s*$', curline).group(1)
            else:
                raise SyntaxError(f"Cannot parse '{curline}' !")
            # create the new file
            path = os.path.join(destdir, path)
            if os.path.exists(path) and not force:
                raise Exception(f'{path} already exists!')
            if not os.path.exists(os.path.dirname(path)):
                os.makedirs(os.path.dirname(path))
            fp = open(path, 'w')
            if fmode is not None: os.chmod(path, fmode)
            if verbose: print(path)
            # proceed with the rest lines
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)
        else:
            if fp is None:
                raise SyntaxError(f'Doesnt know where to write {hft}')
            fp.write(curline)
            hft_unfold(restlines, destdir, fp=fp, verbose=verbose, force=force)


if __name__ == '__main__':

    ag = argparse.ArgumentParser()
    ag.add_argument('-f', '--fold', type=str, default='', required=False,
            help='fold the given file or directory')
    ag.add_argument('-o', '--output', type=str, default='', required=False,
            help='specify output file of the folded result')
    ag.add_argument('-u', '--unfold', type=str, default='', required=False,
            help='unfold the given HFT file')
    ag.add_argument('-d', '--destdir', type=str, default='', required=False,
            help='destination directory when unfolding an HFT file')
    ag.add_argument('-F', '--force', action='store_true', required=False,
            help='force overwrite the output destination')
    ag.add_argument('-S', '--spec', action='store_true', required=False,
            help='print the Hatless Folded Text spec')
    ag.add_argument('-v', '--verbose', action='store_true', required=False,
            help='toggle verbose mode')
    ag.add_argument('-V', '--version', action='store_true', required=False,
            help='print version information')
    ag, agparser = ag.parse_args(), ag

    # Misc info.
    if ag.version:
        print(f'Hatless Folded Text, Version {__VERSION__}')
        exit(0)
    elif ag.spec:
        print(__HFT_SPEC__)
        exit(0)

    # Do folding / unfolding
    if not any([ag.fold, ag.unfold]):
        agparser.print_help()
    elif all([ag.fold, ag.unfold]):
        raise ValueError("Both fold and unfold have been specified!")
    elif ag.fold:
        if not ag.output:
            ag.output = re.sub('/*$', '', ag.fold) + '.hft'
        if os.path.exists(ag.output) and not ag.force:
            raise Exception(f"{ag.output} already exists!")
        hft = hft_fold(ag.fold, verbose=ag.verbose)
        with open(ag.output, 'w') as f:
            f.writelines(hft)
    elif ag.unfold:
        with open(ag.unfold, 'r') as f:
            hft = f.readlines()
        hft_unfold(hft, ag.destdir, verbose=ag.verbose, force=ag.force)
