START OF WEEK 3 of Linux <<< FILE TIMESTAMP >>> -The up to date file is: /usr/courses/cps393/dwoit/courseNotes/ -If you are viewing your own copy, check it is up to date -If you are viewing from a link in the Course Outline, be aware it may be outdated. <<< TRANSFORMERS AND TRANSLATORS >>> << SED (STREAM EDITOR) >> -sends each input line to stdout, possibly changed (edited) -input from stdin or file. Input is unchanged. sed -e "s/linux/LINUX/" myfile changes first occurrence of "linux" to "LINUX" on each line. the -e means the string following is an edit command the "s" means substitute second string for first myfile is name of input file may use regular expressions (like in grep) can use either single quotes or double quotes for now #\U is uppercase. & is matched string sed -e "s/linux/\U&/" #same as above command. \L for lowercase #EVERY occurrence of "the dog" to "my cat" on each line (g=global) sed -e "s/the dog/my cat/g" myfile #third occurrence of "the dog" to "my cat" on each line sed -e "s/the dog/my cat/3" myfile #first occurrence of x or xx or xxx etc on a line to 1234 in myfile sed -e "s/xx*/1234/" myfile #first occurrence of x or xx or xxx etc on a line to matched #string with 12 prepended and 34 appended sed -e "s/xx*/12&34/" myfile #first occurence of "HI" to "hi there" on lines 1-5 of stdin sed -e "1,5 s/HI/hi there/" #first occurence of "HI" to "hi there" on lines of stdin EXCEPT #lines 1-5 sed -e "1,5 !s/HI/hi there/" #only apply the change to lines containing the string "UNIX" sed -e "/UNIX/ s/shell/Shell/" f1.txt #only apply the change to lines that are exactly WoW sed -e "/^WoW$/ s/W/MM/g" f1.txt sed -e "s/^WoW$/MMoMM/g" f1.txt #does same thing sed -e "/PuTTy/ d" #deletes all lines containing PuTTy sed -e "/[dD]ave/ d" #deletes all lines containing Dave or dave sed -e "1,5d" #deletes lines 1-5 #prints lines 2-5 of stdin. p=print, -n=suppress automatic output sed -n '2,5p' #outputs y on stdout for every stdin line containing exactly A sed -e 's/^A$/B/' -e 's/B/y/' can put edit commands into a file (named whatever you want). e.g., File sedcmds: s/shell/Shell/ s/c language/ C Language/ Then: sed -f sedcmds infile can edit a file in place with -i (with optional backup file) sed -i.bak -e 's/X/x/' myF #copies myF to myF.bak first, then #modifies myF by applying the edit command #use any suffix (".bak" just an example) to include '/' in the substitute string, either protect it with \ or use another character to delimit, such as ? these all change every "his/her" to "their" sed -e "s/his\/her/their/g" sed -e "s?his/her?their?g" sed -e "sXhis/herXtheirXg" Substring like in grep. Capture with \( \) Replace with \1 \2 etc etc. Note some versions of sed use \{\1\} instead of \1 #changes x to YxY or xx to YxxY or xxx to YxxxY etc sed -e "s/\(xx*\)/Y\1Y/" #substring like in grep #these both change first linux to upper-case: sed -e "s/\(linux\)/\U\1/" sed -e "s/linux/\U&/" #from earlier example #changes <<>> at line start to **some string #where "some string" can be any string, including null >echo '<<>>' | sed -e 's/^<<<\(.*\)>>>/**\1/' **some string #swaps first 3 chars with next 3 chars >echo 'ABCabc' | sed -e 's/\(...\)\(...\)/\2\1/' abcABC This site has some good sed examples: https://linuxconfig.org/learning-linux-commands-sed This is example site mentioned in sed man page: https://sed.sourceforge.io/grabbag/tutorials/sedfaq.txt HMWK: For each of these, give a sed command to change lines given on stdin 1. Give a sed command that changes every occurrance of the string "xyz" to "zyx". 2. Give a sed command that changes every occurrance of the string "xyz" to "zyx" on all lines EXCEPT lines 3-5. 3. Give a sed command that makes all alpha characters lower-case. 4. Give a sed command that deletes all lines containing three or more digits in a row. 5. Give a sed command that deletes the third occurrance of string "xyz" on every line. 6. Give a sed command that puts double quotes around any string of two or more repeated A characters. e.g., this line: abcAbcAAzzzzAAAAzzABCC becomes : abcAbc"AA"zzzz"AAAA"zzABCC << TR (TRANSLATE) >> -translates STDIN, character by character, and writes to stdout -given translation table specified by from-string and to-string -uses a 1:1 correspondence between from/to strings -if to-string too short, implicit repetition of last char of from-string tr ":" ";" #changes all colons into semi-colons tr " x" "AX" #changes all blanks to "A" and all "x" to "X" tr "A-Z" "a-z" #changes all uppercase to lowercase letters #(so stdout is a lowercase version of stdin) tr "a-zA-Z" "A-Za-z" #changes all upper to lower, and lower to upper tr "a-z" "123" #changes a to 1, b to 2, c-z to 3 can use backslash escapes, \n (newline), \t (tab) etc tr " " "\n" #changes all blanks to newlines can use octal codes, if begin with \ (see man ascii) tr " x" "\012y" #changes all blanks into newlines (\012 is octal newline) #and all "x" to "y" can use Character Classes (see man tr) tr '[:lower:]' '[:upper:]' tr -d "\n" #removes all newline chars (-d=delete) tr -d "a-z" #removes all lower case letters tr -s " " " " # -s=squeeze #replaces all strings of one or more spaces by a single space #in general, replace repeated occurrences of char1 by one char2 tr -s "Ae" " Y" #all strings of one or more "A"s are replaced by one space #and all strings of one or more "e"s are replaced by one "Y" #prints each word of last 5 lines of Alan Turing bio on its own line #by squeezing all punctuation and horizontal whitespace into one newline tail -5 AlanTuringBio80 | tr -s '[:punct:][:blank:]' '\n' (see man tr for more options) <<< ENVIRONMENT VARIABLES >>> -values maintained by the shell. Some are: HOME -full path name of user`s home dir PATH -command search path LOGNAME -userid of current user PS1 -primary prompt string PS2 -secondary prompt string (used by bash to let you know your command is incomplete) PWD -current/present (working) dir /home/dwoit: echo ${PRINTER} ENG281 /home/dwoit: echo ${PS1} ${PWD}: /home/dwoit: PS1='>> ' >> >> PS1='${PWD}: ' /home/dwoit: -env command lists all env vars and values HMWK: 1. write a shell program called myenv which takes one argument. The argument should be the name of an environment variable, such as PATH HOME etc. myenv should print out the value of the variable given as the argument. (Hint: use a pipe.) Your program need not detect if the argument is missing or invalid; if it is, your program is allowed to behave strangely. e.g., myenv PRINTER should print a line such as: PRINTER=kc3500 e.g., myenv HOME PRINTER should print a line such as: HOME=/home/dwoit <<< LOCAL VARIABLES >>> -declare (optional): delcare myname=Denise #(or typeset). Declaration is optional -assign: myname=Denise #NO SPACES around = or interpreted as shell cmd myname='Dr. Woit' #quote if spaces or special chars -use: echo "Professor is ${myname}" #{} optional but prevents ambiguities #double-quotes allow variables substitution #(see QUOTING below) -ambiguities? fn=Denise ; ln=Woit ; echo $fn$ln #DeniseWoit Try printing "M" between first and last name, to get: DeniseMWoit echo $fnM$ln #wrong. Why? echo ${fn}M$ln #correct echo ${fn}M${ln} #correct echo "${fn}M${ln}" #correct -discard (undefine) variable: unset myname #now myname is null -length operator # echo ${#fn} #prints 6 (length of Denise) -select substring after offset name="BA3456GHIJ" echo ${name:3} #prints 456GHIJ (everything after offset of 3) echo ${name:3:5} #prints 456GH (5 chars after offset of 3) -many others. Search "Parameter Expansion" in man bash <<< QUOTING >>> -Must protect white-space characters. Why? Bash scans cmd line and divides into words by white space and evaluates and substitutes for special chars -Suppose want to search for "Denise Woit" in file myfile grep Denise Woit myfile #WRONG #searches Denise in 2 files: Woit, myfile grep 'Denise Woit' myfile #RIGHT (or "Denise Woit") -single quotes most restrictive: evaluates nothing within > X=2 ; echo '$X *' #prints $X * -double quotes: evaluates $ (variable or command substitution), ` (back-quote char) and \ (backslash char) does not evaluate globs (*, ?, etc) > X=2 ; echo "$X *" # prints 2 * > cd # now in /home/dwoit > grep "${PWD}" # searches for /home/dwoit in stdin > grep '${PWD}' # searches for ${PWD} in stdin preserves correct whitespace in variables e.g., > var="l 1 Linux > l 2 Linux > l 3" > echo "$var" l 1 l 2 l 3 > echo $var l 1 l 2 l 3 -backquote (Command Substitution): -evaluates string in backquotes as a cmd, then -substitutes in whatever that command printed to stdout e.g., > TODAY=`date` > echo $TODAY Sat 16 Sep 2023 01:19:59 PM EDT v.s. > TODAY=date > echo $TODAY date e.g., grep `pwd` * does? -assuming in dir /home/dwoit it searches for string /home/dwoit/ in all the entries in /home/dwoit/ $(command) is alternate syntax for `command` Better, because can nest it. This does: grep "junkD" blob /home/dwoit> cd junkD /home/dwoit/junkD> grep "$(echo $(echo $PWD | sed -e 'sx/home/dwoit/xx'))" blob " <<< TEST or [ ] >>> -tests for conditions including: if argument is: readable, writable, executable, a file, a directory if 2 strings or ints are: >, < or = can do AND, OR, NOT logic shell variable $? is assigned result of test: 0 = true 1 = false (since 0 always true/good in bash) test -x fname #does fname have execute perms for user? echo $? #0 printed if yes; 1 if no also -r, -w, -f, -d, -e, etc (see man test) test n1 -eq n2 # $? 0 (true) if integer n1 equal to n2 also -ne, -lt, -gt, -ge, -o -a test -x fname -a -d bin # $? 0 (true) if fname is executable AND bin is a dir -r for readable; -w for writable -f for regular file; -d for directory -e for exists -ne for not equal; -lt for less-than; -o for or Unquoted null-value variables can mess up test syntax: NAME="Denise" test $NAME = "Denise" echo $? 0 #correct, since they are equal Problem: if NAME never set or null then test $NAME = "Denise" becomes: test = "Denise" and get syntax error Solution: test "$NAME" = "Denise" which has OK syntax, becoming test "" = "Denise" #which is false -alternate syntax: [ ] e.g., [ -x fname -a -d bin ] #NEED whitespace after [ and before ] - man test gives all the options <<< EXPR >>> -evaluates arguments and prints true or false (1 or 0 like C, Python) to stdout (not in $? ) e.g., a=1 b=2 expr $a = $b 0 STR="MYNAME" expr "$STR" = "MYNAME" 1 -can do simple math, e.g., a=3 expr $a + 1 4 -operators: *,/,%,+,-,=,!=,<,>,<=,>= \_______/\_____________/ int true/false > a=`expr $a \* 5` #or a=`expr $a '*' 5` or a=$(expr $a \* 5) > echo $a #prints 15 since a was 3 <<< ARITHMETIC EVALUATION >>> bash can do integer arithmetic: > declare -i x=1 #or typeset -i > x=x+1 #no need for $x in math > echo $x 2 vs. without declare -i: > y=1 > y=y+1 > echo $y y+1 -man bash and search for ^SHELL BUILTIN COMMANDS then search under that for declare or typeset $(( )) does integer arithmetic too > unset a ; b=5 > a=$(($b * 5)) ; echo $a 25 Has +, -, *, /, %, **, ==, !=, <, >, etc. -man bash search for Arithmetic Expansion -man bash search for ^ARITHMETIC EVALUATION <<< CONTROL STRUCTURES >>> ; separates commands -saw previously () execute in subshell -has all variables from calling shell -any variables it sets are gone when done -anything that alters shell environment is gone when done {} execute in current shell -has all variables from calling shell -any variables it sets are still set in calling shell when done -some things that alter shell environment are still set when done e.g., /home/dwoit: x=4;y=2; ( x=9;y=9; echo $x $y; ); echo $x $y 9 9 4 2 /home/dwoit: x=4;y=2; { x=9;y=9; echo $x $y; }; echo $x $y 9 9 9 9 | ---------------------------------------------------------- | OPTIONAL | Note: if you put a SHELL PROGRAM within {}, and that shell program | changes the environment (sets a variable for example), it is | NOT changed in calling shell of {} (because SHELL PGMS cannot | change calling environment). | | .e.g, | if shell program xyxy contained statements above: | x=9;y=9; echo $x $y; | then all these would give same output: "9 9 \n 4 2" | x=4;y=2; ./xyxy; echo $x $y | x=4;y=2; ( ./xyxy; ); echo $x $y | x=4;y=2; { ./xyxy; }; echo $x $y | | To get a shell program to run in current environment, do | . ./shell.pgm | e.g., | /home/dwoit: x=4;y=2; . ./xyxy; echo $x $y | 9 9 | 9 9 | Note source does same thing as . above | /home/dwoit: x=4;y=2; source ./xyxy; echo $x $y | ---------------------------------------------------------- both () and {} group stdout cmds into 1 stream " " stderr " " e.g., (cat f1.txt; cat f2.txt) | grep -c dwoit -remember -c counts matching lines and prints a number -prints num of lines in f1.txt AND f2.txt containing "dwoit" -without the ( ) what happens? -f1.txt is catted to stdout, and then -the num of lines of f2.txt containing "dwoit" is printed Note: do not need spaces around ( ) but DO need space after { and DO need ; before } { cat f1; cat f2;} | grep -c dwoit && Tests for true and executes e.g., cmd1 && cmd2 -execute cmd2 if cmd1 returns true (true=0) || Tests for false and executes e.g., cmd1 || cmd2 -execute cmd2 if cmd1 returns false (false=1) e.g., cmd1 || ( cmd2; cmd3 ) is it the same as cmd1 || cmd2; cmd3 ?? NO! e.g., x=3 [ $x = 3 ] || ( echo "x was not 3" ; echo "x was $x" ) #nothing printed on stdout x=9 [ $x = 3 ] || ( echo "x was not 3" ; echo "x was $x" ) x was not 3 x was 9 ! expr Returns false if expr true; true if expr false e.g., ls kjkj 2>/dev/null ; echo $? 2 ! ls kjkj 2>/dev/null ; echo $? 0 [ cmd ] is a short form for test cmd [[ cmd ]] is same but more functionality (for example, it can do > < (greater/less than) for strings, and pattern matching whereas [ ] cannot e.g., [[ $x = *abc ]] true if $x ends in abc see man bash search: \[\[ << IF >> if test-conditions then cmd-list1 elif test-conditions #optional then #optional cmd-list2 #optional else #optional cmd-list3 #optional fi e.g., if [ -f fname -a -x fname ] ; then echo fname is an executable file else echo fname is not executable and/or not a file fi x=dog #a string if [ "$x" = "dog" ] then echo YES #always echoed fi declare -i a=2 if [ $a -gt 0 ] #greater than then echo a is positive fi -eq, -lt etc for numbers and = != for strings null string "" is false; any non-null string is true, e.g., #!/bin/bash #source ff if [ "`grep $1 file1`" ] then echo string $1 is contained in file file1 else echo string $1 is not contained in file file1 fi #works because grep prints null to stdout if not found, and #some line(s) if it is found #what happens if $1 is contained on multiple lines of file1? #what happens if $1 has spaces, like "is just" #what happens if run this with $1 not set? if on multiple lines, works fine, since string is not null if $1 has spaces, get syntax error. To fix, see ff2 if $1 not set, greps for 'file1' in stdin, so waits for input to fix that, see ff1 or ff2 See man test for test operators Some interesting ones: -n string True, if length of string is non-zero. -z string True, if length of string is zero. -e entry True, if entry exists (is a file, dir, etc). file1 -nt file2 True, if file1 is newer than file2. See man bash and search for Compound Commands for if-statement HMWK: Write a shell program called sg that takes 2 command line arguments. The first argument is a textual string; the second a file name. If other than 2 args are supplied it prints on stderr: sg: Usage: sg str file (the sg is printed so that if you change the name of the program, the "Usage line" changes automatically.) If the second arg is not a readable file, print on stderr: sg: file xxx invalid or not readable where "xxx" is arg2 Your program will return 0 if string is contained somewhere in the file, and 1 if something wrong with args, and 2 if string not in file (but args OK). (use grep in your program). Remember from u2.txt: $0 is the name of shell pgm $# is a count of the number of arguments passed HMWK: Re-write shell program ff above so that it does something reasonable when the user invokes it without an argument (or with a null argument as in: ff "" ) HMWK: Write a shell program that will tell you how long another user`s session has been idle. If the user has more than one idle session, just look at the first one. To see idle time, use the who command with options -H -u. Idle time is given in minutes. If a user is idle for less than a minute, a . appears instead of a time. The program will take a userid as an argument and will do one of (a), (b), or (c) below: (a) print "user userid is not logged in" and exit 1 (b) print "user userid has been idle at least 1 minute and exit 0 (c) print "user userid has been idle less than 1 minute and exit 0 HMWK: 1.re-write your homework program called nw (from u2) so that if no argument is passed, all of your entries in the current dir are printed (instead of only 10 as before). Change it so the argument is a simple integer, instead of -integer as before. 2. write a shell program called x that makes your most recently created file executable. << CASE >> #!/bin/bash #Source: datecase mnth=`date +%m` #formats it as mm (month) case ${mnth} in 01) echo "January" ;; 02) echo "February" ;; 03) echo "March" ;; 04) echo "April" ;; 09) echo "September" ;; 10) echo "October" ;; 11) echo "November" ;; 12) echo "December" ;; *) echo "some other month" ;; esac exit 0 case ${userdate} in # userdate is a variable here 01|Jan|January) # multiple matches echo "first month" ;; 02|Feb|February) . . . esac Can use glob constructs (that is why default is *) 01|Jan*) match 01 or anything starting with Jan ???) match anything with exactly 3 chars [0-9][0-9][0-9]) match exactly 3 digits case $(($count < 100)) in 1) echo $count is less than 100;; 0) echo $count is more than 99;; esac EOH 2 << FOR >> for variable in value1 value2 value3 ... valueN do ... done # loops N times, # 1st: $variable is value1 # 2nd: $variable is value2 etc. for variable #in a shell program, loops over do #its command-line arguments (CLAs) ... done #!/bin/bash #Source tryfor echo "not much" >f1 cp f1 f2; cp f1 f3; cp f1 f4 ls -l f[1-4] for file in f1 f2 f3 f4 do chmod -w $file ls -l $file done rm -i f1 f2 f3 f4 exit 0 #!/bin/bash #source cla #program to print out all command line args one at a time for i do echo "$i" #quotes preserve whitespace within $i done exit 0 Note: for i in "$@" also loops over command line args (processes whitespace properly, unlike $*) #!/bin/bash #Source filedir #prints if each entry in current dir is file or dir for file in * do if [ -f $file ] then echo "$file is file" else if [ -d $file ] then echo " $file is dir. " fi fi done exit 0 #!/bin/bash #source renamePgm #renames all files whose name contains "sample" so that the #"sample" part of the name becomes "Ch1.sample" for i in *sample* ; do if [ -f "$i" ] ; then cp $i `echo $i | sed -e 's/sample/Ch1.sample/'` 2>/dev/null if [ $? = "0" ] ; then echo copied $i else echo could not copy $i >&2 fi fi done RANGES in for-loops: e.g., #!/bin/bash #source: forRange #loops over 1 2 3 4 5 echo -e '\n using {start..end} syntax' for i in {1..5} do echo "i is: $i " done #loops over 0 2 4 6 8 10 echo -e '\n using {start..end..inc} syntax' for i in {0..10..2} do echo "i is: $i" done HMWK: 1.write a shell pgm called lst which acts like ls -F, using a shell for loop (Note ls -F puts a '/' after dir names and a '*' after any files that are user executable and a '@' before any that are symbolic links. Look at 'man bash' or 'man test' to find the test for symbolic links) 2. write a shell program called num that prints the number of entries in the current directory. USE A LOOP and COUNTER. Use no pipes. 3. modify your program nw from previous homework so that if the argument is a number larger than the number of entries IN the current directory, the following message is printed: there are only $num entries in this directory The new nw should use your program num from above to assign the number of files/dirs to a variable. In current directory, how would you copy all files in the current dir into a backup directory called /home/userid/bak (where userid is replaced by your userid), assuming that if bak exists, it is used; if not, it is created first? #!/bin/bash #Source: backup test -d /home/dwoit/bak || mkdir /home/dwoit/bak for f in * do if [ -f $f ] then cp ${f} /home/dwoit/bak/${f} fi done exit 0 #note: should error-check (bak writable, properly created, etc.) << READ >> read line < fn echo $line #prints line 1 of file fn #in general reads 1 line of stdin #returns non-0 value (in $?) if EOF (end-of-file) read -n1 char # read returns after reading 1 character rather than waiting # for a complete line of input. read -nX chars # read returns after reading X characters rather ... read -p "Enter your string: " myinput #prints prompt string and reads abcde fgh i #user types input and enter echo $myinput #prints abcde fgh i read arg1 arg2 arg3 #reads "words" on one line (strings separated #by whitespace. reads word1 into arg1, word2 into arg2, and #the REST OF THE LINE into arg3 read a b c > while [ ... ] do ... done #!/bin/bash #Source: list10 #Note: command seq 1 10 does the same thing x=1 while [ $x -le 10 ] do echo $x x=$((x+1)) done exit 0 #if did declare -i x=1 could have simply done x=x+1 while [ 1 ] infinite-loop Note: nothing special about "1"; any non-null string works. while [ 0 ] while [ mouse ] while [ "mouse" ] also infinite loops while [ ] while [ "" ] are both false (null string) " y=`grep second fn1` #all lines of fn1 containing second put in var y if [ "$y" ] etc true if second found at least once in fn1 otws $y is null and if-condition is false #!/bin/bash #Source: countod #read in a number of strings from user (until #user enters END or end) and output the total #number of strings that are files & the total #number that are directories. #note: if the user enters more than one string on a line, # will get if [ -f str1 str2 etc ] bash syntax error declare -i ordfile=0 declare -i dirfile=0 while [ true ] do echo Enter a file name or END to quit read filenam if [ "$filenam" = "END" -o "$filenam" = "end" ] then break #breaks out of the while loop, like in C #continue statement also available fi if [ -f "$filenam" ] then ordfile=ordfile+1 elif [ -d "$filenam" ] then dirfile=dirfile+1 fi done echo -e "The number of ordinary files:\t $ordfile" echo -e "The number of directories:\t $dirfile" exit 0 #The read could have been the while loop condition, as in # while read filenam ; do #See program countod1 #Source: profgone # does nothing until prof logs off your machine # should run this in the background while [ "`who | grep dwoit`" ] ; do sleep 60 #pause 1 minute done echo "she's finally gone \a \a" exit 0 #!/bin/bash # Source: add # adds : to front of each line of several files & concatenates # result into big-file for file in fn? #<-- fn1, fn2 fny etc do cat $file | \ while read line #<-- reads a line of stdin # into var line do echo ":$line" done | cat >> big-file #just done >> big-file works too done #need >> or last file will clobber echo "after while loop value of line is $line" #line is null--see above exit 0 << EXTENDING VARIABLE VALUES BEYOND WHILE LOOP >> Suppose modify countod above to read from a file, supplied on command line: ./countodFile0 countodInput Problem: Doesn`t work now because $ordfile and $dirfile are null in final echo statements. To Fix: many possibilities, e.g., subshell or redirection: Use a subshell: ./countodFile1 countodInput It uses a subshell to extend variable scope past end of while loop cat $1 | ( while read filenam ... done echo -e "The number of ordinary files:\t $ordfile" echo -e "The number of directories:\t $dirfile" ) Redirect read`s input: ./countodFile2 countodInput It redirects loop`s input to come from a file while read filenam ... done < $1 << OTHER CONTROL CONSTRUCTS >> man bash gives additional control constructs, e.g., for (( expr1 ; expr2 ; expr3 )) ; do list ; done select name [ in word ] ; do list ; done until [ expr ] ; do list ; done HMWK: 0. If the numbers from 1 to 4321 were written out, how many times would the digit '5' appear? Write a shell program to figure this out. 1. write a shell program called numit which reads lines from stdin and outputs the lines with line numbers. e.g., if stdin was this is a line this is now another line and here is line Then numit would print 1. this is a line 2. this is now another line 3. and here is line And if file dog contained Abcde klm n Then numit who dwoit pts/0 .... eharley pts/3 .... dwoit pts/2 .... This shows dwoit has 2 windows open (pts/0 and pts/2) The user is given as a command line argument to your program ($1). END OF WEEK 3 (UNIX) May do u3Lab now